curlconverter
Version:
convert curl commands to Python, JavaScript, Go, PHP and more
363 lines (322 loc) • 9.7 kB
text/typescript
import { Word, eq } from "../../shell/Word.js";
import { parse, getFirst, COMMON_SUPPORTED_ARGS } from "../../parse.js";
import type { Request, Warnings } from "../../parse.js";
import { parseQueryString } from "../../Query.js";
import {
reprStr,
repr,
reprAsStringToStringDict,
reprObj,
asParseFloatTimes1000,
asParseInt,
toURLSearchParams,
type JSImports,
addImport,
bySecondElem,
} from "./javascript.js";
export const supportedArgs = new Set([
...COMMON_SUPPORTED_ARGS,
"max-time",
"connect-timeout",
"location",
"no-location",
"location-trusted", // not exactly supported, just better warning message
"no-location-trusted",
"max-redirs",
"compressed",
"insecure",
"http2",
"http2-prior-knowledge",
"form",
"form-string",
// TODO:
// "cookie-jar", // and cookie files
// TODO: tls using https: and agent:
// TODO: proxy stuff using agent:
// TODO: methodRewriting: true to match curl?
]);
function getBodyString(
request: Request,
imports: JSImports,
): [string | null, string | null] {
const contentType = request.headers.getContentType();
// can have things like ; charset=utf-8 which we want to preserve
const exactContentType = request.headers.get("content-type");
if (request.multipartUploads) {
if (eq(exactContentType, "multipart/form-data")) {
// TODO: comment it out instead?
request.headers.delete("content-type");
}
return ["body: form", null];
}
if (!request.data) {
return [null, null];
}
// TODO: @
const simpleString = "body: " + repr(request.data, imports);
try {
if (contentType === "application/json" && request.data.isString()) {
const dataStr = request.data.toString();
const parsed = JSON.parse(dataStr);
const roundtrips = JSON.stringify(parsed) === dataStr;
const jsonAsJavaScript = "json: " + reprObj(parsed, 1);
if (roundtrips && eq(exactContentType, "application/json")) {
request.headers.delete("content-type");
}
return [jsonAsJavaScript, roundtrips ? null : simpleString];
}
if (contentType === "application/x-www-form-urlencoded") {
const [queryList, queryDict] = parseQueryString(request.data);
if (queryDict && queryDict.every((v) => !Array.isArray(v[1]))) {
if (eq(exactContentType, "application/x-www-form-urlencoded")) {
request.headers.delete("content-type");
}
return [
"form: " +
reprAsStringToStringDict(queryDict as [Word, Word][], 1, imports),
null,
];
}
if (queryList) {
return [
"body: " +
toURLSearchParams([queryList, null], imports) +
".toString()",
null,
];
}
}
} catch {}
return [simpleString, null];
}
function buildOptionsObject(
request: Request,
method: Word,
methodStr: string,
methods: string[],
nonDataMethods: string[],
warnings: Warnings,
imports: JSImports,
): string {
let code = "{\n";
if (!method.isString || !methods.includes(methodStr.toUpperCase())) {
code += " method: " + repr(method, imports) + ",\n";
}
if (
request.urls[0].queryDict &&
request.urls[0].queryDict.every((v) => !Array.isArray(v[1]))
) {
code +=
" searchParams: " +
reprAsStringToStringDict(
request.urls[0].queryDict as [Word, Word][],
1,
imports,
) +
",\n";
} else if (request.urls[0].queryList) {
code +=
" searchParams: " +
toURLSearchParams([request.urls[0].queryList, null], imports) +
",\n";
}
const [bodyString, commentedOutBodyString] = getBodyString(request, imports); // can delete headers
if (request.headers.length) {
const headers = request.headers.headers.filter((h) => h[1] !== null) as [
Word,
Word,
][];
if (headers.length) {
code +=
" headers: " + reprAsStringToStringDict(headers, 1, imports) + ",\n";
}
}
if (request.urls[0].auth) {
const [username, password] = request.urls[0].auth;
code += " username: " + repr(username, imports) + ",\n";
if (password.toBool()) {
code += " password: " + repr(password, imports) + ",\n";
}
if (request.authType !== "basic") {
// TODO: warn
}
}
if (request.data || request.multipartUploads) {
if (commentedOutBodyString) {
code += " // " + commentedOutBodyString + ",\n";
}
code += " " + bodyString + ",\n";
// TODO: Does this work for HEAD?
if (nonDataMethods.includes(methodStr.toUpperCase())) {
code += " allowGetBody: true,\n";
}
}
if (request.timeout || request.connectTimeout) {
code += " timeout: {\n";
if (request.timeout) {
code +=
" request: " +
asParseFloatTimes1000(request.timeout, imports) +
",\n";
}
if (request.connectTimeout) {
code +=
" connect: " +
asParseFloatTimes1000(request.connectTimeout, imports) +
",\n";
}
if (code.endsWith(",\n")) {
code = code.slice(0, -2);
code += "\n";
}
code += " },\n";
}
// By default, curl doesn't follow redirects but got does.
let followRedirects = request.followRedirects;
if (followRedirects === undefined) {
followRedirects = true;
}
let maxRedirects = request.maxRedirects
? asParseInt(request.maxRedirects, imports)
: null;
const hasMaxRedirects =
followRedirects &&
maxRedirects &&
maxRedirects !== "0" &&
maxRedirects !== "10"; // got default
if (!followRedirects || maxRedirects === "0") {
code += " followRedirect: false,\n";
} else if (maxRedirects) {
if (maxRedirects === "-1") {
maxRedirects = "Infinity";
}
}
if (followRedirects && request.followRedirectsTrusted) {
warnings.push([
"--location-trusted",
// TODO: is this true?
"got doesn't have an easy way to disable removing the Authorization: header on redirect",
]);
}
if (hasMaxRedirects) {
code += " maxRedirects: " + maxRedirects + ",\n";
}
if (request.compressed === false) {
code += " decompress: false,\n";
}
if (request.insecure) {
code += " https: {\n";
code += " rejectUnauthorized: false\n";
code += " },\n";
}
if (request.http2) {
code += " http2: true,\n";
}
if (code.endsWith(",\n")) {
code = code.slice(0, -2);
}
code += "\n}";
return code;
}
export function _toNodeGot(
requests: Request[],
warnings: Warnings = [],
): string {
const request = getFirst(requests, warnings);
const imports: JSImports = [];
let code = "";
if (request.multipartUploads) {
code += "const form = new FormData();\n";
for (const m of request.multipartUploads) {
code += "form.append(" + repr(m.name, imports) + ", ";
if ("contentFile" in m) {
addImport(imports, "* as fs", "fs");
if (eq(m.contentFile, "-")) {
code += "fs.readFileSync(0).toString()";
} else {
code += "fs.readFileSync(" + repr(m.contentFile, imports) + ")";
}
if ("filename" in m && m.filename) {
code += ", " + repr(m.filename, imports);
}
} else {
code += repr(m.content, imports);
}
code += ");\n";
}
code += "\n";
// TODO: remove warning once Node 16 is EOL'd on 2023-09-11
warnings.push(["node-form-data", "Node 18 is required for FormData"]);
}
const method = request.urls[0].method;
const methodStr = method.toString();
if (method.isString() && methodStr !== methodStr.toUpperCase()) {
warnings.push([
"lowercase-method",
"got will uppercase the method: " + JSON.stringify(methodStr),
]);
}
// https://github.com/sindresorhus/got/blob/e24b89669931b36530219b9f49965d07da25a7e6/source/create.ts#L28
const methods = ["GET", "POST", "PUT", "PATCH", "HEAD", "DELETE"];
// Got will error if you try sending data with these HTTP methods
const nonDataMethods = ["GET", "HEAD"];
code += "const response = await got";
if (
method.isString() &&
methods.includes(methodStr.toUpperCase()) &&
methodStr.toUpperCase() !== "GET"
) {
code += "." + methodStr.toLowerCase();
}
code += "(";
const url = request.urls[0].queryList
? request.urls[0].urlWithoutQueryList
: request.urls[0].url;
code += repr(url, imports);
const needsOptions = !!(
!method.isString() ||
!methods.includes(methodStr.toUpperCase()) ||
request.urls[0].queryList ||
request.urls[0].queryDict ||
request.headers.length ||
request.urls[0].auth ||
request.multipartUploads ||
request.data ||
request.followRedirects ||
request.maxRedirects ||
request.compressed ||
request.insecure ||
request.http2 ||
request.timeout ||
request.connectTimeout
);
if (needsOptions) {
code += ", ";
code += buildOptionsObject(
request,
method,
methodStr,
methods,
nonDataMethods,
warnings,
imports,
);
}
code += ");\n";
let importCode = "import got from 'got';\n";
for (const [varName, imp] of Array.from(imports).sort(bySecondElem)) {
importCode += "import " + varName + " from " + reprStr(imp) + ";\n";
}
return importCode + "\n" + code;
}
export function toNodeGotWarn(
curlCommand: string | string[],
warnings: Warnings = [],
): [string, Warnings] {
const requests = parse(curlCommand, supportedArgs, warnings);
const nodeGot = _toNodeGot(requests, warnings);
return [nodeGot, warnings];
}
export function toNodeGot(curlCommand: string | string[]): string {
return toNodeGotWarn(curlCommand)[0];
}