Add a typed platform package to decouple the frontend from Tauri (#539)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-08-14 12:44:01 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent 0068be9ffc
commit 2383f06e71
107 changed files with 954 additions and 505 deletions
+4 -4
View File
@@ -1,5 +1,5 @@
import { getIdentifier } from "@tauri-apps/api/app";
import { invokeCmd } from "./tauri";
import { platform } from "@yaakapp-internal/platform";
import { rpc } from "./rpc";
export interface AppInfo {
isDev: boolean;
@@ -16,8 +16,8 @@ export interface AppInfo {
}
export const appInfo = {
...(await invokeCmd("cmd_metadata")),
identifier: await getIdentifier(),
...(await rpc("cmd_metadata")),
identifier: await platform.appIdentifier(),
} as AppInfo;
console.log("App info", appInfo);
+3 -3
View File
@@ -1,14 +1,14 @@
import { clear, writeText } from "@tauri-apps/plugin-clipboard-manager";
import { showToast } from "./toast";
import { platform } from "@yaakapp-internal/platform";
export function copyToClipboard(
text: string | null,
{ disableToast }: { disableToast?: boolean } = {},
) {
if (text == null) {
clear().catch(console.error);
platform.clipboard.clear().catch(console.error);
} else {
writeText(text).catch(console.error);
platform.clipboard.writeText(text).catch(console.error);
}
if (text !== "" && !disableToast) {
+2 -2
View File
@@ -1,8 +1,8 @@
import type { HttpRequestHeader } from "@yaakapp-internal/models";
import { invokeCmd } from "./tauri";
import { rpc } from "./rpc";
/**
* Global default headers fetched from the backend.
* These are static and fetched once on module load.
*/
export const defaultHeaders: HttpRequestHeader[] = await invokeCmd("cmd_default_headers");
export const defaultHeaders: HttpRequestHeader[] = await rpc("cmd_default_headers");
+3 -3
View File
@@ -2,7 +2,7 @@ import { parseTemplate } from "@yaakapp-internal/templates";
import { activeEnvironmentIdAtom } from "../hooks/useActiveEnvironment";
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
import { jotaiStore } from "./jotai";
import { invokeCmd } from "./tauri";
import { rpc } from "./rpc";
export function analyzeTemplate(template: string): "global_secured" | "local_secured" | "insecure" {
let secureTags = 0;
@@ -39,7 +39,7 @@ export async function convertTemplateToInsecure(template: string) {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom) ?? "n/a";
const environmentId = jotaiStore.get(activeEnvironmentIdAtom) ?? null;
return invokeCmd<string>("cmd_decrypt_template", { template, workspaceId, environmentId });
return rpc<string>("cmd_decrypt_template", { template, workspaceId, environmentId });
}
export async function convertTemplateToSecure(template: string): Promise<string> {
@@ -53,5 +53,5 @@ export async function convertTemplateToSecure(template: string): Promise<string>
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom) ?? "n/a";
const environmentId = jotaiStore.get(activeEnvironmentIdAtom) ?? null;
return invokeCmd<string>("cmd_secure_template", { template, workspaceId, environmentId });
return rpc<string>("cmd_secure_template", { template, workspaceId, environmentId });
}
+3 -3
View File
@@ -1,11 +1,11 @@
import vkBeautify from "vkbeautify";
import { invokeCmd } from "./tauri";
import { rpc } from "./rpc";
export async function tryFormatJson(text: string): Promise<string> {
if (text === "") return text;
try {
const result = await invokeCmd<string>("cmd_format_json", { text });
const result = await rpc<string>("cmd_format_json", { text });
return result;
} catch (err) {
console.warn("Failed to format JSON", err);
@@ -24,7 +24,7 @@ export async function tryFormatGraphql(text: string): Promise<string> {
if (text === "") return text;
try {
return await invokeCmd<string>("cmd_format_graphql", { text });
return await rpc<string>("cmd_format_graphql", { text });
} catch (err) {
console.warn("Failed to format GraphQL", err);
}
+2 -2
View File
@@ -9,7 +9,7 @@ import { showDialog } from "./dialog";
import { jotaiStore } from "./jotai";
import { pluralizeCount } from "./pluralize";
import { router } from "./router";
import { invokeCmd } from "./tauri";
import { rpc } from "./rpc";
export const importData = createFastMutation({
mutationKey: ["import_data"],
@@ -50,7 +50,7 @@ export const importData = createFastMutation({
async function performImport(filePath: string): Promise<boolean> {
const activeWorkspace = jotaiStore.get(activeWorkspaceAtom);
const imported = await invokeCmd<BatchUpsertResult>("cmd_import_data", {
const imported = await rpc<BatchUpsertResult>("cmd_import_data", {
filePath,
workspaceId: activeWorkspace?.id,
});
+20 -22
View File
@@ -1,5 +1,3 @@
import { emit } from "@tauri-apps/api/event";
import { openUrl } from "@tauri-apps/plugin-opener";
import { debounce } from "@yaakapp-internal/lib";
import type {
FormInput,
@@ -20,23 +18,23 @@ import { Button } from "../components/core/Button";
import { ButtonInfiniteLoading } from "../components/core/ButtonInfiniteLoading";
// Listen for toasts
import { listenToTauriEvent } from "../hooks/useListenToTauriEvent";
import { platform } from "@yaakapp-internal/platform";
import { updateAvailableAtom } from "./atoms";
import { stringToColor } from "./color";
import { generateId } from "./generateId";
import { jotaiStore } from "./jotai";
import { showPrompt } from "./prompt";
import { showPromptForm } from "./prompt-form";
import { invokeCmd } from "./tauri";
import { rpc } from "./rpc";
import { showToast } from "./toast";
export function initGlobalListeners() {
listenToTauriEvent<ShowToastRequest>("show_toast", (event) => {
showToast({ ...event.payload });
platform.listen<ShowToastRequest>("show_toast", (payload) => {
showToast({ ...payload });
});
// Show errors for any plugins that failed to load during startup
void invokeCmd<[string, string][]>("cmd_plugin_init_errors").then((errors) => {
void rpc<[string, string][]>("cmd_plugin_init_errors").then((errors) => {
for (const [dir, err] of errors) {
const name = dir.split(/[/\\]/).pop() ?? dir;
showToast({
@@ -61,13 +59,13 @@ export function initGlobalListeners() {
}
});
listenToTauriEvent("settings", () => openSettings.mutate(null));
platform.listen("settings", () => openSettings.mutate(null));
// Track active dynamic form dialogs so follow-up input updates can reach them
const activeForms = new Map<string, (inputs: FormInput[]) => void>();
// Listen for plugin events
listenToTauriEvent<InternalEvent>("plugin_event", async ({ payload: event }) => {
platform.listen<InternalEvent>("plugin_event", async (event) => {
if (event.payload.type === "prompt_text_request") {
const value = await showPrompt(event.payload);
const result: InternalEvent = {
@@ -81,7 +79,7 @@ export function initGlobalListeners() {
value,
},
};
await emit(event.id, result);
await platform.emit(event.id, result);
} else if (event.payload.type === "prompt_form_request") {
if (event.replyId != null) {
// Follow-up update from plugin runtime — update the active dialog's inputs
@@ -106,7 +104,7 @@ export function initGlobalListeners() {
done,
},
};
void emit(event.id, result);
void platform.emit(event.id, result);
};
const values = await showPromptForm({
@@ -127,24 +125,24 @@ export function initGlobalListeners() {
}
});
listenToTauriEvent<string>("update_installed", async ({ payload: version }) => {
platform.listen<string>("update_installed", async (version) => {
console.log("Got update installed event", version);
showUpdateInstalledToast(version);
});
// Listen for update events
listenToTauriEvent<UpdateInfo>("update_available", async ({ payload }) => {
platform.listen<UpdateInfo>("update_available", async (payload) => {
console.log("Got update available", payload);
void showUpdateAvailableToast(payload);
});
listenToTauriEvent<YaakNotification>("notification", ({ payload }) => {
platform.listen<YaakNotification>("notification", (payload) => {
console.log("Got notification event", payload);
showNotificationToast(payload);
});
// Listen for plugin update events
listenToTauriEvent<PluginUpdateNotification>("plugin_updates_available", ({ payload }) => {
platform.listen<PluginUpdateNotification>("plugin_updates_available", (payload) => {
console.log("Got plugin updates event", payload);
showPluginUpdatesToast(payload);
});
@@ -171,7 +169,7 @@ function showUpdateInstalledToast(version: string) {
loadingChildren="Restarting..."
onClick={() => {
hide();
setTimeout(() => invokeCmd("cmd_restart", {}), 200);
setTimeout(() => rpc("cmd_restart", {}), 200);
}}
>
Relaunch Yaak
@@ -187,7 +185,7 @@ async function showUpdateAvailableToast(updateInfo: UpdateInfo) {
jotaiStore.set(updateAvailableAtom, { version, downloaded });
// Acknowledge the event, so we don't time out and try the fallback update logic
await emit<UpdateResponse>(replyEventId, { type: "ack" });
await platform.emit(replyEventId, { type: "ack" } satisfies UpdateResponse);
showToast({
id: UPDATE_TOAST_ID,
@@ -209,10 +207,10 @@ async function showUpdateAvailableToast(updateInfo: UpdateInfo) {
className="min-w-40"
loadingChildren={downloaded ? "Installing..." : "Downloading..."}
onClick={async () => {
await emit<UpdateResponse>(replyEventId, {
await platform.emit(replyEventId, {
type: "action",
action: "install",
});
} satisfies UpdateResponse);
}}
>
{downloaded ? "Install Now" : "Download and Install"}
@@ -223,7 +221,7 @@ async function showUpdateAvailableToast(updateInfo: UpdateInfo) {
variant="border"
rightSlot={<Icon icon="external_link" />}
onClick={async () => {
await openUrl(`https://yaak.app/changelog/${version}`);
await platform.openUrl(`https://yaak.app/changelog/${version}`);
}}
>
What&apos;s New
@@ -304,7 +302,7 @@ function showNotificationToast(n: YaakNotification) {
</VStack>
),
onClose: () => {
invokeCmd("cmd_dismiss_notification", { notificationId: n.id }).catch(console.error);
rpc("cmd_dismiss_notification", { notificationId: n.id }).catch(console.error);
},
action: ({ hide }) => {
return actionLabel && actionUrl ? (
@@ -315,7 +313,7 @@ function showNotificationToast(n: YaakNotification) {
rightSlot={<Icon icon="external_link" />}
onClick={() => {
hide();
return openUrl(actionUrl);
return platform.openUrl(actionUrl);
}}
>
{actionLabel}
+4 -4
View File
@@ -1,4 +1,3 @@
import { save } from "@tauri-apps/plugin-dialog";
import { Icon } from "@yaakapp-internal/ui";
import mime from "mime";
import { createElement } from "react";
@@ -7,8 +6,9 @@ import type { SniffedValue } from "../components/core/Editor/sniffValue";
import { isEncodedRun } from "../components/core/Editor/sniffValue";
import { copyToClipboard } from "./copy";
import { fireAndForget } from "./fireAndForget";
import { invokeCmd } from "./tauri";
import { rpc } from "./rpc";
import { showToast } from "./toast";
import { platform } from "@yaakapp-internal/platform";
/**
* How the value is written, which is the thing worth knowing about it — that it is base64
@@ -229,7 +229,7 @@ function toBase64(bytes: Uint8Array): string {
*/
export async function saveValue(text: string, sniffed: SniffedValue | null, name: string) {
const ext = sniffed == null ? "txt" : (mime.getExtension(sniffed.mime) ?? "bin");
const filepath = await save({ defaultPath: `${name}.${ext}`, title: "Save Value" });
const filepath = await platform.dialog.save({ defaultPath: `${name}.${ext}`, title: "Save Value" });
if (filepath == null) {
return; // Cancelled
}
@@ -241,6 +241,6 @@ export async function saveValue(text: string, sniffed: SniffedValue | null, name
? normalizeBase64(payloadOf(text, sniffed))
: toBase64(decodeValue(text, sniffed));
await invokeCmd("cmd_save_base64_to_binary", { filepath, data });
await rpc("cmd_save_base64_to_binary", { filepath, data });
showToast({ message: `Saved to ${filepath}` });
}
+7 -7
View File
@@ -1,9 +1,9 @@
import { readFile } from "@tauri-apps/plugin-fs";
import type { HttpResponse } from "@yaakapp-internal/models";
import type { FilterResponse } from "@yaakapp-internal/plugins";
import type { ServerSentEvent, SseSummary } from "@yaakapp-internal/sse";
import { candidateJsonPayloadsFromSseText, computeSseSummary } from "@yaakapp-internal/sse";
import { invokeCmd } from "./tauri";
import { rpc } from "./rpc";
import { platform } from "@yaakapp-internal/platform";
export async function getResponseBodyText({
response,
@@ -12,7 +12,7 @@ export async function getResponseBodyText({
response: HttpResponse;
filter: string | null;
}): Promise<string | null> {
const result = await invokeCmd<FilterResponse>("cmd_http_response_body", {
const result = await rpc<FilterResponse>("cmd_http_response_body", {
response,
filter,
});
@@ -29,7 +29,7 @@ export async function getResponseBodyEventSource(
): Promise<ServerSentEvent[]> {
if (!response.bodyPath) return [];
try {
const events = await invokeCmd<ServerSentEvent[]>("cmd_get_sse_events", {
const events = await rpc<ServerSentEvent[]>("cmd_get_sse_events", {
filePath: response.bodyPath,
});
if (events.length > 0) {
@@ -39,7 +39,7 @@ export async function getResponseBodyEventSource(
// Fall back to raw JSON frame parsing for non-standard SSE-like responses.
}
const bytes = await readFile(response.bodyPath);
const bytes = await platform.files.readFile(response.bodyPath);
const text = new TextDecoder("utf-8").decode(bytes);
return candidateJsonPayloadsFromSseText(text).map((data, index) => ({
data,
@@ -55,7 +55,7 @@ export async function getResponseBodySseSummary(
): Promise<SseSummary> {
if (!response.bodyPath) return { fragmentCount: 0, summary: "" };
const bytes = await readFile(response.bodyPath);
const bytes = await platform.files.readFile(response.bodyPath);
const text = new TextDecoder("utf-8").decode(bytes);
return computeSseSummary(text, resultKeyPath);
}
@@ -64,5 +64,5 @@ export async function getResponseBodyBytes(
response: HttpResponse,
): Promise<Uint8Array<ArrayBuffer> | null> {
if (!response.bodyPath) return null;
return readFile(response.bodyPath);
return platform.files.readFile(response.bodyPath);
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { type } from "@tauri-apps/plugin-os";
import { platform } from "@yaakapp-internal/platform";
const os = type();
const os = platform.osType();
export const revealInFinderText =
os === "macos"
? "Reveal in Finder"
@@ -1,7 +1,15 @@
import type { InvokeArgs } from "@tauri-apps/api/core";
import { invoke } from "@tauri-apps/api/core";
import type { RpcPayload } from "@yaakapp-internal/platform";
import { platform } from "@yaakapp-internal/platform";
type TauriCmd =
/**
* Every backend command the client sends.
*
* Listing them keeps typos out and gives us the inventory to check the Rust
* side against. Once the app's commands move onto `RpcRouter`, this union is
* replaced by the generated `RpcSchema` and the payload and result types come
* with it, the way `apps/yaak-proxy/lib/rpc.ts` already works.
*/
type AppCmd =
| "cmd_call_grpc_request_action"
| "cmd_call_http_authentication_action"
| "cmd_call_http_request_action"
@@ -53,14 +61,15 @@ type TauriCmd =
| "cmd_send_http_request"
| "cmd_template_function_summaries"
| "cmd_template_function_config"
| "cmd_template_tokens_to_string";
| "cmd_template_tokens_to_string"
| "models_get_graphql_introspection"
| "models_get_settings"
| "models_grpc_events"
| "models_upsert_graphql_introspection"
| "models_websocket_events"
| "plugin:yaak-license|check";
export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> {
// console.log('RUN COMMAND', cmd, args);
try {
return await invoke(cmd, args);
} catch (err) {
console.warn("Tauri command error", cmd, err);
throw err;
}
/** Call a backend command. */
export function rpc<T>(cmd: AppCmd, payload?: RpcPayload): Promise<T> {
return platform.rpc<T>(cmd, payload);
}
+2 -2
View File
@@ -1,6 +1,6 @@
import type { HttpRequest, HttpResponse } from "@yaakapp-internal/models";
import { getActiveCookieJar } from "../hooks/useActiveCookieJar";
import { invokeCmd } from "./tauri";
import { rpc } from "./rpc";
export async function sendEphemeralRequest(
request: HttpRequest,
@@ -8,7 +8,7 @@ export async function sendEphemeralRequest(
): Promise<HttpResponse> {
// Remove some things that we don't want to associate
const newRequest = { ...request };
return invokeCmd("cmd_send_ephemeral_request", {
return rpc("cmd_send_ephemeral_request", {
request: newRequest,
environmentId,
cookieJarId: getActiveCookieJar()?.id,
+2 -2
View File
@@ -1,6 +1,6 @@
import { invoke } from "@tauri-apps/api/core";
import type { Settings } from "@yaakapp-internal/models";
import { rpc } from "./rpc";
export function getSettings(): Promise<Settings> {
return invoke<Settings>("models_get_settings");
return rpc<Settings>("models_get_settings");
}
+2 -2
View File
@@ -5,10 +5,10 @@ import {
resolveAppearance,
type Appearance,
} from "@yaakapp-internal/theme";
import { invokeCmd } from "./tauri";
import { rpc } from "./rpc";
export async function getThemes() {
const themes = (await invokeCmd<GetThemesResponse[]>("cmd_get_themes")).flatMap((t) => t.themes);
const themes = (await rpc<GetThemesResponse[]>("cmd_get_themes")).flatMap((t) => t.themes);
themes.sort((a, b) => a.label.localeCompare(b.label));
// Remove duplicates, in case multiple plugins provide the same theme
const uniqueThemes = Array.from(new Map(themes.map((t) => [t.id, t])).values());