import { availableTargets, type HarRequest, HTTPSnippet } from "@readme/httpsnippet"; import type { EditorLanguage, HttpRequest, PluginDefinition } from "@yaakapp/api"; // Get all available targets and build select options const targets = availableTargets(); // Targets to exclude from the language list const excludedTargets = new Set(["json"]); // Build language (target) options const languageOptions = targets .filter((target) => !excludedTargets.has(target.key)) .map((target) => ({ label: target.title, value: target.key, })); // Preferred clients per target (shown first in the list) const preferredClients: Record = { javascript: "fetch", node: "fetch", }; // Get client options for a given target key function getClientOptions(targetKey: string) { const target = targets.find((t) => t.key === targetKey); if (!target) return []; const preferred = preferredClients[targetKey]; return target.clients .map((client) => ({ label: client.title, value: client.key, })) .sort((a, b) => { if (a.value === preferred) return -1; if (b.value === preferred) return 1; return 0; }); } // Get default client for a target function getDefaultClient(targetKey: string): string { const options = getClientOptions(targetKey); return options[0]?.value ?? ""; } // Defaults const defaultTarget = "javascript"; // Map httpsnippet target key to editor language for syntax highlighting const editorLanguageMap: Record = { c: "c", clojure: "clojure", csharp: "csharp", go: "go", http: "http", java: "java", javascript: "javascript", kotlin: "kotlin", node: "javascript", objc: "objective_c", ocaml: "ocaml", php: "php", powershell: "powershell", python: "python", r: "r", ruby: "ruby", shell: "shell", swift: "swift", }; function getEditorLanguage(targetKey: string): EditorLanguage { return editorLanguageMap[targetKey] ?? "text"; } // Convert Yaak HttpRequest to HAR format function toHarRequest(request: Partial) { // Build URL with query parameters let finalUrl = request.url || ""; const urlParams = (request.urlParameters ?? []).filter((p) => p.enabled !== false && !!p.name); if (urlParams.length > 0) { const [base, hash] = finalUrl.split("#"); const separator = base?.includes("?") ? "&" : "?"; const queryString = urlParams .map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value)}`) .join("&"); finalUrl = base + separator + queryString + (hash ? `#${hash}` : ""); } // Build headers array const headers: Array<{ name: string; value: string }> = (request.headers ?? []) .filter((h) => h.enabled !== false && !!h.name) .map((h) => ({ name: h.name, value: h.value })); // Handle authentication if (request.authentication?.disabled !== true) { if (request.authenticationType === "basic") { const credentials = btoa( `${request.authentication?.username ?? ""}:${request.authentication?.password ?? ""}`, ); headers.push({ name: "Authorization", value: `Basic ${credentials}` }); } else if (request.authenticationType === "bearer") { const prefix = request.authentication?.prefix ?? "Bearer"; const token = request.authentication?.token ?? ""; headers.push({ name: "Authorization", value: `${prefix} ${token}`.trim() }); } else if (request.authenticationType === "apikey") { if (request.authentication?.location === "header") { headers.push({ name: request.authentication?.key ?? "X-Api-Key", value: request.authentication?.value ?? "", }); } else if (request.authentication?.location === "query") { const sep = finalUrl.includes("?") ? "&" : "?"; finalUrl = [ finalUrl, sep, encodeURIComponent(request.authentication?.key ?? "token"), "=", encodeURIComponent(request.authentication?.value ?? ""), ].join(""); } } } // Build HAR request object const har: Record = { method: request.method || "GET", url: finalUrl, headers, }; // Handle request body const bodyType = request.bodyType ?? "none"; if (bodyType !== "none" && request.body) { if (bodyType === "application/x-www-form-urlencoded" && Array.isArray(request.body.form)) { const params = request.body.form .filter((p: { enabled?: boolean; name?: string }) => p.enabled !== false && !!p.name) .map((p: { name: string; value: string }) => ({ name: p.name, value: p.value })); har.postData = { mimeType: "application/x-www-form-urlencoded", params, }; } else if (bodyType === "multipart/form-data" && Array.isArray(request.body.form)) { const params = request.body.form .filter((p: { enabled?: boolean; name?: string }) => p.enabled !== false && !!p.name) .map((p: { name: string; value: string; file?: string; contentType?: string }) => { const param: Record = { name: p.name, value: p.value || "" }; if (p.file) param.fileName = p.file; if (p.contentType) param.contentType = p.contentType; return param; }); har.postData = { mimeType: "multipart/form-data", params, }; } else if (bodyType === "graphql" && typeof request.body.query === "string") { const body = { query: request.body.query || "", variables: maybeParseJSON(request.body.variables, undefined), }; har.postData = { mimeType: "application/json", text: JSON.stringify(body), }; } else if (typeof request.body.text === "string") { har.postData = { mimeType: bodyType, text: request.body.text, }; } } return har; } function maybeParseJSON(v: unknown, fallback: T): unknown { if (typeof v !== "string") return fallback; try { return JSON.parse(v); } catch { return fallback; } } export const plugin: PluginDefinition = { httpRequestActions: [ { label: "Generate Code Snippet", icon: "copy", async onSelect(ctx, args) { // Render the request with variables resolved const renderedRequest = await ctx.httpRequest.render({ httpRequest: args.httpRequest, purpose: "send", }); // Convert to HAR format const harRequest = toHarRequest(renderedRequest) as HarRequest; // Get previously selected language or use defaults const storedTarget = await ctx.store.get("selectedTarget"); const initialTarget = storedTarget || defaultTarget; const storedClient = await ctx.store.get(`selectedClient:${initialTarget}`); const initialClient = storedClient || getDefaultClient(initialTarget); // Create snippet generator const snippet = new HTTPSnippet(harRequest); const generateSnippet = (target: string, client: string): string => { const result = snippet.convert( target as Parameters[0], client as Parameters[1], ); return (Array.isArray(result) ? result.join("\n") : result || "").replace(/\r\n/g, "\n"); }; // Generate initial code preview let initialCode = ""; try { initialCode = generateSnippet(initialTarget, initialClient); } catch { initialCode = "// Error generating snippet"; } // Show dialog with language/library selectors and code preview const result = await ctx.prompt.form({ id: "httpsnippet", title: "Generate Code Snippet", confirmText: "Copy to Clipboard", cancelText: "Cancel", size: "md", inputs: [ { type: "h_stack", inputs: [ { type: "select", name: "target", label: "Language", defaultValue: initialTarget, options: languageOptions, }, { type: "select", name: `client-${initialTarget}`, label: "Library", defaultValue: initialClient, options: getClientOptions(initialTarget), dynamic(_ctx, { values }) { const targetKey = String(values.target || defaultTarget); const options = getClientOptions(targetKey); return { name: `client-${targetKey}`, options, defaultValue: options[0]?.value ?? "", }; }, }, ], }, { type: "editor", name: "code", label: "Preview", language: getEditorLanguage(initialTarget), defaultValue: initialCode, readOnly: true, rows: 15, dynamic(_ctx, { values }) { const targetKey = String(values.target || defaultTarget); const clientKey = String( values[`client-${targetKey}`] || getDefaultClient(targetKey), ); let code: string; try { code = generateSnippet(targetKey, clientKey); } catch { code = "// Error generating snippet"; } return { defaultValue: code, language: getEditorLanguage(targetKey), }; }, }, ], }); if (result) { // Store the selected language and library for next time const selectedTarget = String(result.target || initialTarget); const selectedClient = String( result[`client-${selectedTarget}`] || getDefaultClient(selectedTarget), ); await ctx.store.set("selectedTarget", selectedTarget); await ctx.store.set(`selectedClient:${selectedTarget}`, selectedClient); // Generate snippet for the selected language try { const codeText = generateSnippet(selectedTarget, selectedClient); await ctx.clipboard.copyText(codeText); await ctx.toast.show({ message: "Code snippet copied to clipboard", icon: "copy", color: "success", }); } catch (err) { await ctx.toast.show({ message: `Failed to generate snippet: ${err instanceof Error ? err.message : String(err)}`, icon: "alert_triangle", color: "danger", }); } } }, }, ], };