Created
April 2, 2026 10:54
-
-
Save tdchien/1770ff468279ce13db5f2a26fab3bba9 to your computer and use it in GitHub Desktop.
Script to convert export from Hoppscoth to Bruno
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| const fs = require("fs"); | |
| const path = require("path"); | |
| function readJson(filePath) { | |
| return JSON.parse(fs.readFileSync(filePath, "utf8")); | |
| } | |
| function ensureDir(dir) { | |
| fs.mkdirSync(dir, { recursive: true }); | |
| } | |
| function safeName(name = "untitled") { | |
| return String(name) | |
| .trim() | |
| .replace(/[<>:"/\\|?*\x00-\x1F]/g, "-") | |
| .replace(/\s+/g, "-") | |
| .replace(/-+/g, "-") | |
| .replace(/^-|-$/g, "") || "untitled"; | |
| } | |
| function escapeBruString(value = "") { | |
| return String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'"); | |
| } | |
| function parseUrlParts(rawUrl = "") { | |
| try { | |
| const u = new URL(rawUrl); | |
| return { | |
| protocol: u.protocol.replace(":", ""), | |
| host: u.host, | |
| pathname: u.pathname || "/", | |
| search: u.search || "", | |
| }; | |
| } catch { | |
| return { | |
| protocol: "", | |
| host: "", | |
| pathname: rawUrl || "/", | |
| search: "", | |
| }; | |
| } | |
| } | |
| function buildQueryFromUrl(rawUrl = "") { | |
| try { | |
| const u = new URL(rawUrl); | |
| const items = []; | |
| for (const [key, value] of u.searchParams.entries()) { | |
| items.push({ key, value, enabled: true }); | |
| } | |
| return items; | |
| } catch { | |
| return []; | |
| } | |
| } | |
| function normalizeHeaders(headers) { | |
| if (!Array.isArray(headers)) return []; | |
| return headers | |
| .filter(h => h && (h.key || h.name)) | |
| .map(h => ({ | |
| key: h.key || h.name || "", | |
| value: h.value ?? "", | |
| enabled: h.active !== false && h.enabled !== false | |
| })); | |
| } | |
| function normalizeParams(params) { | |
| if (!Array.isArray(params)) return []; | |
| return params | |
| .filter(p => p && p.key) | |
| .map(p => ({ | |
| key: p.key, | |
| value: p.value ?? "", | |
| enabled: p.active !== false && p.enabled !== false | |
| })); | |
| } | |
| function detectBody(req) { | |
| const body = req.body || {}; | |
| const contentTypeHeader = (req.headers || []).find( | |
| h => (h.key || "").toLowerCase() === "content-type" | |
| ); | |
| const contentType = contentTypeHeader?.value?.toLowerCase() || ""; | |
| if (typeof body === "string") { | |
| return { type: "text", content: body }; | |
| } | |
| if (body?.body != null && body?.contentType === "application/json") { | |
| return { type: "json", content: typeof body.body === "string" ? body.body : JSON.stringify(body.body, null, 2) }; | |
| } | |
| if (body?.body != null && contentType.includes("application/json")) { | |
| return { type: "json", content: typeof body.body === "string" ? body.body : JSON.stringify(body.body, null, 2) }; | |
| } | |
| if (body?.body != null) { | |
| return { type: "text", content: typeof body.body === "string" ? body.body : JSON.stringify(body.body, null, 2) }; | |
| } | |
| return null; | |
| } | |
| function requestToBru(req) { | |
| const name = req.name || "Untitled Request"; | |
| const method = (req.method || "GET").toUpperCase(); | |
| const url = req.endpoint || req.url || ""; | |
| const headers = normalizeHeaders(req.headers); | |
| const params = normalizeParams(req.params); | |
| const urlQuery = buildQueryFromUrl(url); | |
| const mergedParams = [...urlQuery]; | |
| for (const p of params) { | |
| if (!mergedParams.some(x => x.key === p.key && x.value === p.value)) { | |
| mergedParams.push(p); | |
| } | |
| } | |
| const body = detectBody(req); | |
| let out = ""; | |
| out += `meta {\n`; | |
| out += ` name: ${name}\n`; | |
| out += ` type: http\n`; | |
| out += ` seq: 1\n`; | |
| out += `}\n\n`; | |
| out += `http {\n`; | |
| out += ` method: ${method}\n`; | |
| out += ` url: ${url}\n`; | |
| out += `}\n\n`; | |
| if (mergedParams.length) { | |
| out += `params:query {\n`; | |
| for (const p of mergedParams) { | |
| if (!p.enabled) continue; | |
| out += ` ${p.key}: ${p.value}\n`; | |
| } | |
| out += `}\n\n`; | |
| } | |
| if (headers.length) { | |
| out += `headers {\n`; | |
| for (const h of headers) { | |
| if (!h.enabled) continue; | |
| out += ` ${h.key}: ${h.value}\n`; | |
| } | |
| out += `}\n\n`; | |
| } | |
| if (body && ["POST", "PUT", "PATCH", "DELETE"].includes(method)) { | |
| out += `body:${body.type} {\n`; | |
| out += ` ${body.type === "json" ? "raw" : "raw"}: '''\n${body.content}\n'''\n`; | |
| out += `}\n\n`; | |
| } | |
| return out.trim() + "\n"; | |
| } | |
| function writeRequest(req, dir, seqRef) { | |
| const filename = `${safeName(req.name || `request-${seqRef.value}`)}.bru`; | |
| const filePath = path.join(dir, filename); | |
| let content = requestToBru(req); | |
| content = content.replace("seq: 1", `seq: ${seqRef.value++}`); | |
| fs.writeFileSync(filePath, content, "utf8"); | |
| } | |
| function processFolder(folder, parentDir, seqRef) { | |
| const folderDir = path.join(parentDir, safeName(folder.name || "folder")); | |
| ensureDir(folderDir); | |
| if (Array.isArray(folder.requests)) { | |
| for (const req of folder.requests) { | |
| writeRequest(req, folderDir, seqRef); | |
| } | |
| } | |
| if (Array.isArray(folder.folders)) { | |
| for (const child of folder.folders) { | |
| processFolder(child, folderDir, seqRef); | |
| } | |
| } | |
| } | |
| function convertCollection(collection, outputBaseDir) { | |
| const collectionName = collection.name || "hoppscotch-collection"; | |
| const rootDir = path.join(outputBaseDir, safeName(collectionName)); | |
| ensureDir(rootDir); | |
| const brunoJson = { | |
| version: "1", | |
| name: collectionName, | |
| type: "collection" | |
| }; | |
| fs.writeFileSync( | |
| path.join(rootDir, "bruno.json"), | |
| JSON.stringify(brunoJson, null, 2), | |
| "utf8" | |
| ); | |
| const seqRef = { value: 1 }; | |
| if (Array.isArray(collection.requests)) { | |
| for (const req of collection.requests) { | |
| writeRequest(req, rootDir, seqRef); | |
| } | |
| } | |
| if (Array.isArray(collection.folders)) { | |
| for (const folder of collection.folders) { | |
| processFolder(folder, rootDir, seqRef); | |
| } | |
| } | |
| return rootDir; | |
| } | |
| function main() { | |
| const [, , inputFile, outputDir = "./bruno-output"] = process.argv; | |
| if (!inputFile) { | |
| console.error("Usage: node hoppscotch-to-bruno.js <hoppscotch.json> [output-dir]"); | |
| process.exit(1); | |
| } | |
| const data = readJson(inputFile); | |
| const collections = Array.isArray(data) ? data : [data]; | |
| ensureDir(outputDir); | |
| for (const collection of collections) { | |
| const out = convertCollection(collection, outputDir); | |
| console.log(`Converted: ${collection.name || "unnamed"} -> ${out}`); | |
| } | |
| } | |
| main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment