TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import type { FetchRequestInput } from "@fetcha/core";2import type { CodeLang } from "@/components/ui/code-block";34/**5 * Code generator for the Playground and docs. Pure: no React, no server imports.6 * Only non-default fields are emitted so snippets stay minimal and copy-pasteable.7 */89export type CodegenLang = "curl" | "javascript" | "typescript" | "python" | "python-sdk" | "go" | "php" | "ruby" | "java" | "csharp";1011export interface CodegenLangInfo {12 id: CodegenLang;13 label: string;14 /** Highlighter language for `CodeBlock`. */15 codeLang: CodeLang;16 /** File extension used by "save as" / download. */17 ext: string;18}1920export const CODEGEN_LANGS: readonly CodegenLangInfo[] = [21 { id: "curl", label: "cURL", codeLang: "bash", ext: "sh" },22 { id: "javascript", label: "JavaScript", codeLang: "javascript", ext: "js" },23 { id: "typescript", label: "TypeScript SDK", codeLang: "typescript", ext: "ts" },24 { id: "python", label: "Python", codeLang: "python", ext: "py" },25 { id: "python-sdk", label: "Python SDK", codeLang: "python", ext: "py" },26 { id: "go", label: "Go", codeLang: "go", ext: "go" },27 { id: "php", label: "PHP", codeLang: "php", ext: "php" },28 { id: "ruby", label: "Ruby", codeLang: "ruby", ext: "rb" },29 { id: "java", label: "Java", codeLang: "java", ext: "java" },30 { id: "csharp", label: "C#", codeLang: "csharp", ext: "cs" },31] as const;3233export interface CodegenOptions {34 /** Shown in place of the real key, e.g. `fch_live_••••1234` or `YOUR_API_KEY`. */35 apiKeyPlaceholder: string;36 /** Public base URL, e.g. `https://www.fetcha.co` (no trailing slash). */37 baseUrl: string;38}3940/** Field order used in the emitted JSON body. */41const FIELD_ORDER = [42 "url",43 "method",44 "headers",45 "cookies",46 "body",47 "country",48 "region",49 "city",50 "network",51 "session",52 "browser",53 "browser_fallback",54 "wait_for",55 "wait_ms",56 "wait_until",57 "javascript",58 "block_resources",59 "screenshot",60 "links",61 "referer",62 "device",63 "locale",64 "timeout",65 "format",66 "follow_redirects",67 "max_redirects",68 "max_response_bytes",69 "retries",70 "cache",71 "debug",72] as const;7374type Json = string | number | boolean | null | Json[] | { [k: string]: Json };75export type RequestBody = Record<string, Json>;7677function isEmptyRecord(v: unknown): boolean {78 return typeof v === "object" && v !== null && !Array.isArray(v) && Object.keys(v as object).length === 0;79}8081/**82 * Build the JSON body that the API should receive: the URL plus every field that differs83 * from the API default. Unknown/undefined/empty values are dropped.84 */85export function buildRequestBody(request: FetchRequestInput): RequestBody {86 const out: RequestBody = { url: request.url };87 const r = request as Record<string, unknown>;88 for (const key of FIELD_ORDER) {89 if (key === "url") continue;90 const v = r[key];91 if (v === undefined || v === null || v === "") continue;92 switch (key) {93 case "method":94 if (v !== "GET") out.method = v as string;95 break;96 case "headers":97 case "cookies":98 if (!isEmptyRecord(v)) out[key] = v as Json;99 break;100 case "body":101 if (typeof v === "string" ? v.length > 0 : !isEmptyRecord(v)) out.body = v as Json;102 break;103 case "network":104 if (v !== "auto") out.network = v as string;105 break;106 case "browser":107 case "screenshot":108 case "links":109 case "debug":110 if (v === true) out[key] = true;111 break;112 case "browser_fallback":113 case "javascript":114 case "block_resources":115 if (v === false) out[key] = false;116 break;117 case "wait_until":118 if (v !== "domcontentloaded") out.wait_until = v as string;119 break;120 case "referer":121 if (v !== "auto") out.referer = v as string;122 break;123 case "timeout":124 if (v !== 30_000) out.timeout = v as number;125 break;126 case "format":127 if (v !== "html") out.format = v as string;128 break;129 case "follow_redirects":130 if (v === false) out.follow_redirects = false;131 break;132 case "max_redirects":133 if (v !== 10) out.max_redirects = v as number;134 break;135 case "cache":136 if (typeof v === "object" && (v as { enabled?: boolean }).enabled) out.cache = v as Json;137 break;138 default:139 out[key] = v as Json;140 }141 }142 return out;143}144145// ---------------------------------------------------------------------------146// Literal serializers per language147// ---------------------------------------------------------------------------148149const IND = " ";150151function jsonPretty(v: Json): string {152 return JSON.stringify(v, null, 2);153}154155/** Escape for a double-quoted string in C-like languages (JS, Go, Java, C#, PHP double quotes). */156function cString(s: string): string {157 return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;158}159160function pyString(s: string): string {161 return cString(s);162}163164function phpString(s: string): string {165 return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;166}167168function rubyString(s: string): string {169 // `#{` interpolates inside double quotes — escape it.170 return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/#\{/g, "\\#{").replace(/\n/g, "\\n")}"`;171}172173function toPython(v: Json, depth = 0): string {174 if (v === null) return "None";175 if (typeof v === "boolean") return v ? "True" : "False";176 if (typeof v === "number") return String(v);177 if (typeof v === "string") return pyString(v);178 const pad = IND.repeat(depth + 1);179 const end = IND.repeat(depth);180 if (Array.isArray(v)) {181 if (!v.length) return "[]";182 return `[\n${v.map((x) => `${pad}${toPython(x, depth + 1)}`).join(",\n")}\n${end}]`;183 }184 const entries = Object.entries(v);185 if (!entries.length) return "{}";186 return `{\n${entries.map(([k, x]) => `${pad}${pyString(k)}: ${toPython(x, depth + 1)}`).join(",\n")}\n${end}}`;187}188189function toPhp(v: Json, depth = 0): string {190 if (v === null) return "null";191 if (typeof v === "boolean") return v ? "true" : "false";192 if (typeof v === "number") return String(v);193 if (typeof v === "string") return phpString(v);194 const pad = IND.repeat(depth + 1);195 const end = IND.repeat(depth);196 if (Array.isArray(v)) {197 if (!v.length) return "[]";198 return `[\n${v.map((x) => `${pad}${toPhp(x, depth + 1)}`).join(",\n")}\n${end}]`;199 }200 const entries = Object.entries(v);201 if (!entries.length) return "new stdClass()";202 return `[\n${entries.map(([k, x]) => `${pad}${phpString(k)} => ${toPhp(x, depth + 1)}`).join(",\n")}\n${end}]`;203}204205function toRuby(v: Json, depth = 0): string {206 if (v === null) return "nil";207 if (typeof v === "boolean") return String(v);208 if (typeof v === "number") return String(v);209 if (typeof v === "string") return rubyString(v);210 const pad = IND.repeat(depth + 1);211 const end = IND.repeat(depth);212 if (Array.isArray(v)) {213 if (!v.length) return "[]";214 return `[\n${v.map((x) => `${pad}${toRuby(x, depth + 1)}`).join(",\n")}\n${end}]`;215 }216 const entries = Object.entries(v);217 if (!entries.length) return "{}";218 return `{\n${entries.map(([k, x]) => `${pad}${rubyString(k)} => ${toRuby(x, depth + 1)}`).join(",\n")}\n${end}}`;219}220221/** Go raw string when safe, otherwise an escaped interpreted string (compact JSON). */222function goJsonLiteral(body: RequestBody): string {223 const pretty = jsonPretty(body);224 if (!pretty.includes("`")) return `\`${pretty}\``;225 return cString(JSON.stringify(body));226}227228/** Java text block: backslashes must be doubled; a `"""` sequence must be broken. */229function javaTextBlock(json: string, indent: string): string {230 const escaped = json.replace(/\\/g, "\\\\").replace(/"""/g, '\\"""');231 return `"""\n${escaped232 .split("\n")233 .map((l) => indent + l)234 .join("\n")}\n${indent}"""`;235}236237/** C# raw string literal; use more quotes than the longest run inside the content. */238function csharpRawString(json: string, indent: string): string {239 const longest = Math.max(2, ...(json.match(/"+/g) ?? []).map((m) => m.length));240 const q = '"'.repeat(longest + 1);241 return `${q}\n${json242 .split("\n")243 .map((l) => indent + l)244 .join("\n")}\n${indent}${q}`;245}246247function indentLines(s: string, indent: string): string {248 return s249 .split("\n")250 .map((l, i) => (i === 0 ? l : indent + l))251 .join("\n");252}253254// ---------------------------------------------------------------------------255// Generators256// ---------------------------------------------------------------------------257258function genCurl(body: RequestBody, o: CodegenOptions): string {259 const json = jsonPretty(body).replace(/'/g, "'\\''");260 return [`curl -X POST ${o.baseUrl}/v1/fetch \\`, ` -H "Authorization: Bearer ${o.apiKeyPlaceholder}" \\`, ` -H "Content-Type: application/json" \\`, ` -d '${json}'`].join("\n");261}262263function genJavaScript(body: RequestBody, o: CodegenOptions): string {264 return [265 `const response = await fetch(${cString(`${o.baseUrl}/v1/fetch`)}, {`,266 ` method: "POST",`,267 ` headers: {`,268 ` Authorization: ${cString(`Bearer ${o.apiKeyPlaceholder}`)},`,269 ` "Content-Type": "application/json",`,270 ` },`,271 ` body: JSON.stringify(${indentLines(jsonPretty(body), " ")}),`,272 `});`,273 ``,274 `const result = await response.json();`,275 `if (!response.ok) throw new Error(\`\${result.error.code}: \${result.error.message}\`);`,276 ``,277 `console.log(result.status, result.metadata.network, result.metadata.duration_ms);`,278 `console.log(result.content);`,279 ].join("\n");280}281282function genTypeScriptSdk(body: RequestBody, o: CodegenOptions): string {283 return [284 `// Install from source until published: pnpm add ./packages/sdk (repo) — npm package @fetcha/sdk`,285 `import { Fetcha } from "@fetcha/sdk";`,286 ``,287 `const fetcha = new Fetcha({ apiKey: process.env.FETCHA_API_KEY ?? ${cString(o.apiKeyPlaceholder)} });`,288 ``,289 `const result = await fetcha.fetch(${indentLines(jsonPretty(body), "")});`,290 ``,291 `console.log(result.status, result.metadata.network, result.metadata.duration_ms);`,292 `console.log(result.content);`,293 ].join("\n");294}295296function genPython(body: RequestBody, o: CodegenOptions): string {297 return [298 `import requests`,299 ``,300 `response = requests.post(`,301 ` ${pyString(`${o.baseUrl}/v1/fetch`)},`,302 ` headers={`,303 ` "Authorization": ${pyString(`Bearer ${o.apiKeyPlaceholder}`)},`,304 ` "Content-Type": "application/json",`,305 ` },`,306 ` json=${indentLines(toPython(body), " ")},`,307 ` timeout=${(typeof body.timeout === "number" ? body.timeout : 30_000) / 1000 + 15},`,308 `)`,309 `result = response.json()`,310 `if not response.ok:`,311 ` raise RuntimeError(f"{result['error']['code']}: {result['error']['message']}")`,312 ``,313 `print(result["status"], result["metadata"]["network"], result["metadata"]["duration_ms"])`,314 `print(result["content"])`,315 ].join("\n");316}317318function genPythonSdk(body: RequestBody, o: CodegenOptions): string {319 const { url, ...rest } = body;320 const kwargs = Object.entries(rest).map(([k, v]) => ` ${k}=${indentLines(toPython(v, 1), " ")},`);321 const call = kwargs.length ? `fetcha.fetch(\n ${pyString(url as string)},\n${kwargs.join("\n")}\n)` : `fetcha.fetch(${pyString(url as string)})`;322 return [323 `# Install from source until published: pip install ./sdk-python (repo) — PyPI package "fetcha"`,324 `import os`,325 `from fetcha import Fetcha`,326 ``,327 `fetcha = Fetcha(api_key=os.environ.get("FETCHA_API_KEY", ${pyString(o.apiKeyPlaceholder)}))`,328 ``,329 `result = ${call}`,330 ``,331 `print(result.status, result.metadata.network, result.metadata.duration_ms)`,332 `print(result.content)`,333 ].join("\n");334}335336function genGo(body: RequestBody, o: CodegenOptions): string {337 return [338 `package main`,339 ``,340 `import (`,341 `\t"bytes"`,342 `\t"fmt"`,343 `\t"io"`,344 `\t"net/http"`,345 `)`,346 ``,347 `func main() {`,348 `\tpayload := []byte(${indentLines(goJsonLiteral(body), "")})`,349 ``,350 `\treq, err := http.NewRequest("POST", ${cString(`${o.baseUrl}/v1/fetch`)}, bytes.NewBuffer(payload))`,351 `\tif err != nil {`,352 `\t\tpanic(err)`,353 `\t}`,354 `\treq.Header.Set("Authorization", ${cString(`Bearer ${o.apiKeyPlaceholder}`)})`,355 `\treq.Header.Set("Content-Type", "application/json")`,356 ``,357 `\tres, err := http.DefaultClient.Do(req)`,358 `\tif err != nil {`,359 `\t\tpanic(err)`,360 `\t}`,361 `\tdefer res.Body.Close()`,362 ``,363 `\tbody, _ := io.ReadAll(res.Body)`,364 `\tfmt.Println(res.StatusCode, res.Header.Get("X-Fetcha-Request-ID"))`,365 `\tfmt.Println(string(body))`,366 `}`,367 ].join("\n");368}369370function genPhp(body: RequestBody, o: CodegenOptions): string {371 return [372 `<?php`,373 ``,374 `$payload = ${toPhp(body)};`,375 ``,376 `$ch = curl_init(${phpString(`${o.baseUrl}/v1/fetch`)});`,377 `curl_setopt_array($ch, [`,378 ` CURLOPT_POST => true,`,379 ` CURLOPT_RETURNTRANSFER => true,`,380 ` CURLOPT_HTTPHEADER => [`,381 ` ${phpString(`Authorization: Bearer ${o.apiKeyPlaceholder}`)},`,382 ` 'Content-Type: application/json',`,383 ` ],`,384 ` CURLOPT_POSTFIELDS => json_encode($payload),`,385 `]);`,386 ``,387 `$response = curl_exec($ch);`,388 `$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);`,389 `curl_close($ch);`,390 ``,391 `$result = json_decode($response, true);`,392 `if ($status >= 400) {`,393 ` throw new RuntimeException($result['error']['code'] . ': ' . $result['error']['message']);`,394 `}`,395 ``,396 `echo $result['status'], ' ', $result['metadata']['network'], PHP_EOL;`,397 `echo $result['content'];`,398 ].join("\n");399}400401function genRuby(body: RequestBody, o: CodegenOptions): string {402 return [403 `require "net/http"`,404 `require "json"`,405 ``,406 `uri = URI(${rubyString(`${o.baseUrl}/v1/fetch`)})`,407 `payload = ${toRuby(body)}`,408 ``,409 `request = Net::HTTP::Post.new(uri)`,410 `request["Authorization"] = ${rubyString(`Bearer ${o.apiKeyPlaceholder}`)}`,411 `request["Content-Type"] = "application/json"`,412 `request.body = JSON.generate(payload)`,413 ``,414 `response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", read_timeout: 150) do |http|`,415 ` http.request(request)`,416 `end`,417 ``,418 `result = JSON.parse(response.body)`,419 `raise "#{result["error"]["code"]}: #{result["error"]["message"]}" unless response.is_a?(Net::HTTPSuccess)`,420 ``,421 `puts "#{result["status"]} #{result["metadata"]["network"]} #{result["metadata"]["duration_ms"]}ms"`,422 `puts result["content"]`,423 ].join("\n");424}425426function genJava(body: RequestBody, o: CodegenOptions): string {427 return [428 `import java.net.URI;`,429 `import java.net.http.HttpClient;`,430 `import java.net.http.HttpRequest;`,431 `import java.net.http.HttpResponse;`,432 `import java.time.Duration;`,433 ``,434 `public class FetchaExample {`,435 ` public static void main(String[] args) throws Exception {`,436 ` String payload = ${javaTextBlock(jsonPretty(body), " ")};`,437 ``,438 ` HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();`,439 ` HttpRequest request = HttpRequest.newBuilder()`,440 ` .uri(URI.create(${cString(`${o.baseUrl}/v1/fetch`)}))`,441 ` .header("Authorization", ${cString(`Bearer ${o.apiKeyPlaceholder}`)})`,442 ` .header("Content-Type", "application/json")`,443 ` .timeout(Duration.ofSeconds(150))`,444 ` .POST(HttpRequest.BodyPublishers.ofString(payload))`,445 ` .build();`,446 ``,447 ` HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());`,448 ` System.out.println(response.statusCode() + " " + response.headers().firstValue("X-Fetcha-Request-ID").orElse(""));`,449 ` System.out.println(response.body());`,450 ` }`,451 `}`,452 ].join("\n");453}454455function genCSharp(body: RequestBody, o: CodegenOptions): string {456 return [457 `using System.Net.Http;`,458 `using System.Net.Http.Headers;`,459 `using System.Text;`,460 ``,461 `var payload = ${csharpRawString(jsonPretty(body), " ")};`,462 ``,463 `using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(150) };`,464 `client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ${cString(o.apiKeyPlaceholder)});`,465 ``,466 `var response = await client.PostAsync(`,467 ` ${cString(`${o.baseUrl}/v1/fetch`)},`,468 ` new StringContent(payload, Encoding.UTF8, "application/json")`,469 `);`,470 ``,471 `var json = await response.Content.ReadAsStringAsync();`,472 `Console.WriteLine($"{(int)response.StatusCode} {response.Headers.GetValues("X-Fetcha-Request-ID").FirstOrDefault()}");`,473 `Console.WriteLine(json);`,474 ].join("\n");475}476477/** Generate a ready-to-run snippet calling `POST /v1/fetch` with the given request. */478export function generateCode(lang: CodegenLang, request: FetchRequestInput, opts: CodegenOptions): string {479 const body = buildRequestBody(request);480 const o: CodegenOptions = { apiKeyPlaceholder: opts.apiKeyPlaceholder || "YOUR_API_KEY", baseUrl: opts.baseUrl.replace(/\/$/, "") };481 switch (lang) {482 case "curl":483 return genCurl(body, o);484 case "javascript":485 return genJavaScript(body, o);486 case "typescript":487 return genTypeScriptSdk(body, o);488 case "python":489 return genPython(body, o);490 case "python-sdk":491 return genPythonSdk(body, o);492 case "go":493 return genGo(body, o);494 case "php":495 return genPhp(body, o);496 case "ruby":497 return genRuby(body, o);498 case "java":499 return genJava(body, o);500 case "csharp":501 return genCSharp(body, o);502 }503}504505export function codegenInfo(lang: CodegenLang): CodegenLangInfo {506 return CODEGEN_LANGS.find((l) => l.id === lang) ?? CODEGEN_LANGS[0]!;507}508