refactor: Move cURL converter to frontend (#11432)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Csaba Tuncsik 2025-02-07 12:05:04 +01:00 committed by GitHub
parent 94789e0309
commit 8b28d6ce8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 2499 additions and 1232 deletions

View file

@ -91,6 +91,7 @@
},
"patchedDependencies": {
"bull@4.12.1": "patches/bull@4.12.1.patch",
"curlconverter@4.11.0": "patches/curlconverter@4.11.0.patch",
"pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch",
"pyodide@0.23.4": "patches/pyodide@0.23.4.patch",
"@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch",

View file

@ -112,7 +112,6 @@
"convict": "6.2.4",
"cookie-parser": "1.4.7",
"csrf": "3.1.0",
"curlconverter": "3.21.0",
"dotenv": "8.6.0",
"express": "4.21.1",
"express-async-errors": "3.1.1",

View file

@ -1,52 +0,0 @@
import type { Request } from 'express';
import { mock } from 'jest-mock-extended';
import { CurlController } from '@/controllers/curl.controller';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import type { CurlService } from '@/services/curl.service';
describe('CurlController', () => {
const service = mock<CurlService>();
const controller = new CurlController(service);
beforeEach(() => jest.clearAllMocks());
describe('toJson', () => {
it('should throw BadRequestError when invalid cURL command is provided', () => {
const req = mock<Request>();
service.toHttpNodeParameters.mockImplementation(() => {
throw new Error();
});
expect(() => controller.toJson(req)).toThrow(BadRequestError);
});
it('should return flattened parameters when valid cURL command is provided', () => {
const curlCommand = 'curl -v -X GET https://test.n8n.berlin/users';
const req = mock<Request>();
req.body = { curlCommand };
service.toHttpNodeParameters.mockReturnValue({
url: 'https://test.n8n.berlin/users',
authentication: 'none',
method: 'GET',
sendHeaders: false,
sendQuery: false,
options: {
redirect: { redirect: {} },
response: { response: {} },
},
sendBody: false,
});
const result = controller.toJson(req);
expect(result).toEqual({
'parameters.method': 'GET',
'parameters.url': 'https://test.n8n.berlin/users',
'parameters.authentication': 'none',
'parameters.sendBody': false,
'parameters.sendHeaders': false,
'parameters.sendQuery': false,
});
});
});
});

View file

@ -1,20 +0,0 @@
import { Request } from 'express';
import { Post, RestController } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { CurlService, flattenObject } from '@/services/curl.service';
@RestController('/curl')
export class CurlController {
constructor(private readonly curlService: CurlService) {}
@Post('/to-json')
toJson(req: Request<{}, {}, { curlCommand: string }>) {
try {
const parameters = this.curlService.toHttpNodeParameters(req.body.curlCommand);
return flattenObject(parameters, 'parameters');
} catch (e) {
throw new BadRequestError('Invalid cURL command');
}
}
}

View file

@ -1,3 +0,0 @@
declare module 'curlconverter' {
export function toJsonString(data: string): string;
}

View file

@ -37,7 +37,6 @@ import '@/controllers/active-workflows.controller';
import '@/controllers/annotation-tags.controller.ee';
import '@/controllers/auth.controller';
import '@/controllers/binary-data.controller';
import '@/controllers/curl.controller';
import '@/controllers/ai.controller';
import '@/controllers/dynamic-node-parameters.controller';
import '@/controllers/invitation.controller';
@ -392,6 +391,7 @@ export class Server extends AbstractServer {
method === 'GET' &&
accept &&
(accept.includes('text/html') || accept.includes('*/*')) &&
!req.path.endsWith('.wasm') &&
!nonUIRoutesRegex.test(req.path)
) {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');

View file

@ -1,297 +0,0 @@
import { CurlService } from '@/services/curl.service';
describe('CurlService', () => {
const service = new CurlService();
test('Should parse form-urlencoded content type correctly', () => {
const curl =
'curl -X POST https://reqbin.com/echo/post/form -H "Content-Type: application/x-www-form-urlencoded" -d "param1=value1&param2=value2"';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo/post/form');
expect(parameters.sendBody).toBe(true);
expect(parameters.bodyParameters?.parameters[0].name).toBe('param1');
expect(parameters.bodyParameters?.parameters[0].value).toBe('value1');
expect(parameters.bodyParameters?.parameters[1].name).toBe('param2');
expect(parameters.bodyParameters?.parameters[1].value).toBe('value2');
expect(parameters.contentType).toBe('form-urlencoded');
expect(parameters.sendHeaders).toBe(false);
expect(parameters.sendQuery).toBe(false);
});
test('Should parse JSON content type correctly', () => {
const curl =
'curl -X POST https://reqbin.com/echo/post/json -H \'Content-Type: application/json\' -d \'{"login":"my_login","password":"my_password"}\'';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo/post/json');
expect(parameters.sendBody).toBe(true);
expect(parameters.bodyParameters?.parameters[0].name).toBe('login');
expect(parameters.bodyParameters?.parameters[0].value).toBe('my_login');
expect(parameters.bodyParameters?.parameters[1].name).toBe('password');
expect(parameters.bodyParameters?.parameters[1].value).toBe('my_password');
expect(parameters.contentType).toBe('json');
expect(parameters.sendHeaders).toBe(false);
expect(parameters.sendQuery).toBe(false);
});
test('Should parse multipart-form-data content type correctly', () => {
const curl =
'curl -X POST https://reqbin.com/echo/post/json -v -F key1=value1 -F upload=@localfilename';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo/post/json');
expect(parameters.sendBody).toBe(true);
expect(parameters.bodyParameters?.parameters[0].parameterType).toBe('formData');
expect(parameters.bodyParameters?.parameters[0].name).toBe('key1');
expect(parameters.bodyParameters?.parameters[0].value).toBe('value1');
expect(parameters.bodyParameters?.parameters[1].parameterType).toBe('formBinaryData');
expect(parameters.bodyParameters?.parameters[1].name).toBe('upload');
expect(parameters.contentType).toBe('multipart-form-data');
expect(parameters.sendHeaders).toBe(false);
expect(parameters.sendQuery).toBe(false);
});
test('Should parse binary request correctly', () => {
const curl =
"curl --location --request POST 'https://www.website.com' --header 'Content-Type: image/png' --data-binary '@/Users/image.png";
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://www.website.com');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(true);
expect(parameters.contentType).toBe('binaryData');
expect(parameters.sendHeaders).toBe(false);
expect(parameters.sendQuery).toBe(false);
});
test('Should parse unknown content type correctly', () => {
const curl = `curl -X POST https://reqbin.com/echo/post/xml
-H "Content-Type: application/xml"
-H "Accept: application/xml"
-d "<Request><Login>my_login</Login><Password>my_password</Password></Request>"`;
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo/post/xml');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(true);
expect(parameters.contentType).toBe('raw');
expect(parameters.rawContentType).toBe('application/xml');
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('Accept');
expect(parameters.headerParameters?.parameters[0].value).toBe('application/xml');
expect(parameters.sendQuery).toBe(false);
});
test('Should parse header properties and keep the original case', () => {
const curl =
'curl -X POST https://reqbin.com/echo/post/json -v -F key1=value1 -F upload=@localfilename -H "ACCEPT: text/javascript" -H "content-type: multipart/form-data"';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo/post/json');
expect(parameters.sendBody).toBe(true);
expect(parameters.bodyParameters?.parameters[0].parameterType).toBe('formData');
expect(parameters.bodyParameters?.parameters[0].name).toBe('key1');
expect(parameters.bodyParameters?.parameters[0].value).toBe('value1');
expect(parameters.bodyParameters?.parameters[1].parameterType).toBe('formBinaryData');
expect(parameters.bodyParameters?.parameters[1].name).toBe('upload');
expect(parameters.contentType).toBe('multipart-form-data');
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('ACCEPT');
expect(parameters.headerParameters?.parameters[0].value).toBe('text/javascript');
expect(parameters.sendQuery).toBe(false);
});
test('Should parse querystring properties', () => {
const curl = "curl -G -d 'q=kitties' -d 'count=20' https://google.com/search";
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://google.com/search');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendHeaders).toBe(false);
expect(parameters.sendQuery).toBe(true);
expect(parameters.queryParameters?.parameters[0].name).toBe('q');
expect(parameters.queryParameters?.parameters[0].value).toBe('kitties');
expect(parameters.queryParameters?.parameters[1].name).toBe('count');
expect(parameters.queryParameters?.parameters[1].value).toBe('20');
});
test('Should parse basic authentication property and keep the original case', () => {
const curl = 'curl https://reqbin.com/echo -u "login:password"';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
});
test('Should parse location flag with --location', () => {
const curl = 'curl https://reqbin.com/echo -u "login:password" --location';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
expect(parameters.options.redirect.redirect.followRedirects).toBe(true);
});
test('Should parse location flag with --L', () => {
const curl = 'curl https://reqbin.com/echo -u "login:password" -L';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
expect(parameters.options.redirect.redirect.followRedirects).toBe(true);
});
test('Should parse location and max redirects flags with --location and --max-redirs 10', () => {
const curl = 'curl https://reqbin.com/echo -u "login:password" --location --max-redirs 10';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
expect(parameters.options.redirect.redirect.followRedirects).toBe(true);
expect(parameters.options.redirect.redirect.maxRedirects).toBe('10');
});
test('Should parse proxy flag -x', () => {
const curl = 'curl https://reqbin.com/echo -u "login:password" -x https://google.com';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
expect(parameters.options.proxy).toBe('https://google.com');
});
test('Should parse proxy flag --proxy', () => {
const curl = 'curl https://reqbin.com/echo -u "login:password" -x https://google.com';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
expect(parameters.options.proxy).toBe('https://google.com');
});
test('Should parse include headers on output flag --include', () => {
const curl = 'curl https://reqbin.com/echo -u "login:password" --include -x https://google.com';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
expect(parameters.options.response.response.fullResponse).toBe(true);
});
test('Should parse include headers on output flag -i', () => {
const curl = 'curl https://reqbin.com/echo -u "login:password" -x https://google.com -i';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
expect(parameters.options.response.response.fullResponse).toBe(true);
});
test('Should parse include request flag -X', () => {
const curl = 'curl -X POST https://reqbin.com/echo -u "login:password" -x https://google.com';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
});
test('Should parse include request flag --request', () => {
const curl =
'curl --request POST https://reqbin.com/echo -u "login:password" -x https://google.com';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
});
test('Should parse include timeout flag --connect-timeout', () => {
const curl =
'curl --request POST https://reqbin.com/echo -u "login:password" --connect-timeout 20';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
expect(parameters.options.timeout).toBe(20000);
});
test('Should parse download file flag -O', () => {
const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" -O';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
expect(parameters.options.response.response.responseFormat).toBe('file');
expect(parameters.options.response.response.outputPropertyName).toBe('data');
});
test('Should parse download file flag -o', () => {
const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" -o';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
expect(parameters.options.response.response.responseFormat).toBe('file');
expect(parameters.options.response.response.outputPropertyName).toBe('data');
});
test('Should parse ignore SSL flag -k', () => {
const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" -k';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
expect(parameters.options.allowUnauthorizedCerts).toBe(true);
});
test('Should parse ignore SSL flag --insecure', () => {
const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" --insecure';
const parameters = service.toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
expect(parameters.options.allowUnauthorizedCerts).toBe(true);
});
});

View file

@ -1,479 +0,0 @@
import { Service } from '@n8n/di';
import get from 'lodash/get';
import type { IDataObject } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow';
import curlconverter from 'curlconverter';
interface CurlJson {
url: string;
raw_url?: string;
method: string;
contentType?: string;
cookies?: {
[key: string]: string;
};
auth?: {
user: string;
password: string;
};
headers?: {
[key: string]: string;
};
files?: {
[key: string]: string;
};
queries: {
[key: string]: string;
};
data?: {
[key: string]: string;
};
}
interface Parameter {
parameterType?: string;
name: string;
value: string;
}
interface HttpNodeParameters {
url?: string;
method: string;
sendBody?: boolean;
authentication: string;
contentType?: 'form-urlencoded' | 'multipart-form-data' | 'json' | 'raw' | 'binaryData';
rawContentType?: string;
specifyBody?: 'json' | 'keypair';
bodyParameters?: {
parameters: Parameter[];
};
jsonBody?: object;
options: {
allowUnauthorizedCerts?: boolean;
proxy?: string;
timeout?: number;
redirect: {
redirect: {
followRedirects?: boolean;
maxRedirects?: number;
};
};
response: {
response: {
fullResponse?: boolean;
responseFormat?: string;
outputPropertyName?: string;
};
};
};
sendHeaders?: boolean;
headerParameters?: {
parameters: Parameter[];
};
sendQuery?: boolean;
queryParameters?: {
parameters: Parameter[];
};
}
type HttpNodeHeaders = Pick<HttpNodeParameters, 'sendHeaders' | 'headerParameters'>;
type HttpNodeQueries = Pick<HttpNodeParameters, 'sendQuery' | 'queryParameters'>;
const enum ContentTypes {
applicationJson = 'application/json',
applicationFormUrlEncoded = 'application/x-www-form-urlencoded',
applicationMultipart = 'multipart/form-data',
}
const SUPPORTED_CONTENT_TYPES = [
ContentTypes.applicationJson,
ContentTypes.applicationFormUrlEncoded,
ContentTypes.applicationMultipart,
];
const CONTENT_TYPE_KEY = 'content-type';
const FOLLOW_REDIRECT_FLAGS = ['--location', '-L'];
const MAX_REDIRECT_FLAG = '--max-redirs';
const PROXY_FLAGS = ['-x', '--proxy'];
const INCLUDE_HEADERS_IN_OUTPUT_FLAGS = ['-i', '--include'];
const REQUEST_FLAGS = ['-X', '--request'];
const TIMEOUT_FLAGS = ['--connect-timeout'];
const DOWNLOAD_FILE_FLAGS = ['-O', '-o'];
const IGNORE_SSL_ISSUES_FLAGS = ['-k', '--insecure'];
const isContentType = (headers: CurlJson['headers'], contentType: ContentTypes): boolean => {
return get(headers, CONTENT_TYPE_KEY) === contentType;
};
const isJsonRequest = (curlJson: CurlJson): boolean => {
if (isContentType(curlJson.headers, ContentTypes.applicationJson)) return true;
if (curlJson.data) {
const bodyKey = Object.keys(curlJson.data)[0];
try {
JSON.parse(bodyKey);
return true;
} catch {
return false;
}
}
return false;
};
const isFormUrlEncodedRequest = (curlJson: CurlJson): boolean => {
if (isContentType(curlJson.headers, ContentTypes.applicationFormUrlEncoded)) return true;
if (curlJson.data && !curlJson.files) return true;
return false;
};
const isMultipartRequest = (curlJson: CurlJson): boolean => {
if (isContentType(curlJson.headers, ContentTypes.applicationMultipart)) return true;
// only multipart/form-data request include files
if (curlJson.files) return true;
return false;
};
const isBinaryRequest = (curlJson: CurlJson): boolean => {
if (curlJson?.headers?.[CONTENT_TYPE_KEY]) {
const contentType = curlJson?.headers?.[CONTENT_TYPE_KEY];
return ['image', 'video', 'audio'].some((d) => contentType.includes(d));
}
return false;
};
const sanitizeCurlCommand = (curlCommand: string) =>
curlCommand
.replace(/\r\n/g, ' ')
.replace(/\n/g, ' ')
.replace(/\\/g, ' ')
.replace(/[ ]{2,}/g, ' ');
const toKeyValueArray = ([key, value]: string[]) => ({ name: key, value });
const extractHeaders = (headers: CurlJson['headers'] = {}): HttpNodeHeaders => {
const emptyHeaders = !Object.keys(headers).length;
const onlyContentTypeHeaderDefined =
Object.keys(headers).length === 1 && headers[CONTENT_TYPE_KEY] !== undefined;
if (emptyHeaders || onlyContentTypeHeaderDefined) return { sendHeaders: false };
return {
sendHeaders: true,
headerParameters: {
parameters: Object.entries(headers)
.map(toKeyValueArray)
.filter((parameter) => parameter.name !== CONTENT_TYPE_KEY),
},
};
};
const extractQueries = (queries: CurlJson['queries'] = {}): HttpNodeQueries => {
const emptyQueries = !Object.keys(queries).length;
if (emptyQueries) return { sendQuery: false };
return {
sendQuery: true,
queryParameters: {
parameters: Object.entries(queries).map(toKeyValueArray),
},
};
};
const extractJson = (body: CurlJson['data']) =>
jsonParse<{ [key: string]: string }>(Object.keys(body as IDataObject)[0]);
const jsonBodyToNodeParameters = (body: CurlJson['data'] = {}): Parameter[] | [] => {
const data = extractJson(body);
return Object.entries(data).map(toKeyValueArray);
};
const multipartToNodeParameters = (
body: CurlJson['data'] = {},
files: CurlJson['files'] = {},
): Parameter[] | [] => {
return [
...Object.entries(body)
.map(toKeyValueArray)
.map((e) => ({ parameterType: 'formData', ...e })),
...Object.entries(files)
.map(toKeyValueArray)
.map((e) => ({ parameterType: 'formBinaryData', ...e })),
];
};
const keyValueBodyToNodeParameters = (body: CurlJson['data'] = {}): Parameter[] | [] => {
return Object.entries(body).map(toKeyValueArray);
};
const lowerCaseContentTypeKey = (obj: { [x: string]: string }): void => {
const regex = new RegExp(CONTENT_TYPE_KEY, 'gi');
const contentTypeKey = Object.keys(obj).find((key) => {
const group = Array.from(key.matchAll(regex));
if (group.length) return true;
return false;
});
if (!contentTypeKey) return;
const value = obj[contentTypeKey];
delete obj[contentTypeKey];
obj[CONTENT_TYPE_KEY] = value;
};
const encodeBasicAuthentication = (username: string, password: string) =>
Buffer.from(`${username}:${password}`).toString('base64');
const jsonHasNestedObjects = (json: { [key: string]: string | number | object }) =>
Object.values(json).some((e) => typeof e === 'object');
const extractGroup = (curlCommand: string, regex: RegExp) => curlCommand.matchAll(regex);
const mapCookies = (cookies: CurlJson['cookies']): { cookie: string } | {} => {
if (!cookies) return {};
const cookiesValues = Object.entries(cookies).reduce(
(accumulator: string, entry: [string, string]) => {
accumulator += `${entry[0]}=${entry[1]};`;
return accumulator;
},
'',
);
if (!cookiesValues) return {};
return {
cookie: cookiesValues,
};
};
export const flattenObject = (obj: { [x: string]: any }, prefix = '') =>
Object.keys(obj).reduce((acc, k) => {
const pre = prefix.length ? prefix + '.' : '';
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
if (typeof obj[k] === 'object') Object.assign(acc, flattenObject(obj[k], pre + k));
//@ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
else acc[pre + k] = obj[k];
return acc;
}, {});
@Service()
export class CurlService {
// eslint-disable-next-line complexity
toHttpNodeParameters(curlCommand: string): HttpNodeParameters {
const curlJson = jsonParse<CurlJson>(curlconverter.toJsonString(curlCommand));
if (!curlJson.headers) curlJson.headers = {};
lowerCaseContentTypeKey(curlJson.headers);
// set basic authentication
if (curlJson.auth) {
const { user, password: pass } = curlJson.auth;
Object.assign(curlJson.headers, {
authorization: `Basic ${encodeBasicAuthentication(user, pass)}`,
});
}
const httpNodeParameters: HttpNodeParameters = {
url: curlJson.url,
authentication: 'none',
method: curlJson.method.toUpperCase(),
...extractHeaders({ ...curlJson.headers, ...mapCookies(curlJson.cookies) }),
...extractQueries(curlJson.queries),
options: {
redirect: {
redirect: {},
},
response: {
response: {},
},
},
};
//attempt to get the curl flags not supported by the library
const curl = sanitizeCurlCommand(curlCommand);
//check for follow redirect flags
if (FOLLOW_REDIRECT_FLAGS.some((flag) => curl.includes(` ${flag}`))) {
Object.assign(httpNodeParameters.options.redirect?.redirect, { followRedirects: true });
if (curl.includes(` ${MAX_REDIRECT_FLAG}`)) {
const extractedValue = Array.from(
extractGroup(curl, new RegExp(` ${MAX_REDIRECT_FLAG} (\\d+)`, 'g')),
);
if (extractedValue.length) {
const [_, maxRedirects] = extractedValue[0];
if (maxRedirects) {
Object.assign(httpNodeParameters.options.redirect?.redirect, { maxRedirects });
}
}
}
}
//check for proxy flags
if (PROXY_FLAGS.some((flag) => curl.includes(` ${flag}`))) {
const foundFlag = PROXY_FLAGS.find((flag) => curl.includes(` ${flag}`));
if (foundFlag) {
const extractedValue = Array.from(
extractGroup(curl, new RegExp(` ${foundFlag} (\\S*)`, 'g')),
);
if (extractedValue.length) {
const [_, proxy] = extractedValue[0];
Object.assign(httpNodeParameters.options, { proxy });
}
}
}
// check for "include header in output" flag
if (INCLUDE_HEADERS_IN_OUTPUT_FLAGS.some((flag) => curl.includes(` ${flag}`))) {
Object.assign(httpNodeParameters.options?.response?.response, {
fullResponse: true,
responseFormat: 'autodetect',
});
}
// check for request flag
if (REQUEST_FLAGS.some((flag) => curl.includes(` ${flag}`))) {
const foundFlag = REQUEST_FLAGS.find((flag) => curl.includes(` ${flag}`));
if (foundFlag) {
const extractedValue = Array.from(
extractGroup(curl, new RegExp(` ${foundFlag} (\\w+)`, 'g')),
);
if (extractedValue.length) {
const [_, request] = extractedValue[0];
httpNodeParameters.method = request.toUpperCase();
}
}
}
// check for timeout flag
if (TIMEOUT_FLAGS.some((flag) => curl.includes(` ${flag}`))) {
const foundFlag = TIMEOUT_FLAGS.find((flag) => curl.includes(` ${flag}`));
if (foundFlag) {
const extractedValue = Array.from(
extractGroup(curl, new RegExp(` ${foundFlag} (\\d+)`, 'g')),
);
if (extractedValue.length) {
const [_, timeout] = extractedValue[0];
Object.assign(httpNodeParameters.options, {
timeout: parseInt(timeout, 10) * 1000,
});
}
}
}
// check for download flag
if (DOWNLOAD_FILE_FLAGS.some((flag) => curl.includes(` ${flag}`))) {
const foundFlag = DOWNLOAD_FILE_FLAGS.find((flag) => curl.includes(` ${flag}`));
if (foundFlag) {
Object.assign(httpNodeParameters.options.response.response, {
responseFormat: 'file',
outputPropertyName: 'data',
});
}
}
if (IGNORE_SSL_ISSUES_FLAGS.some((flag) => curl.includes(` ${flag}`))) {
const foundFlag = IGNORE_SSL_ISSUES_FLAGS.find((flag) => curl.includes(` ${flag}`));
if (foundFlag) {
Object.assign(httpNodeParameters.options, {
allowUnauthorizedCerts: true,
});
}
}
const contentType = curlJson?.headers?.[CONTENT_TYPE_KEY] as ContentTypes;
if (isBinaryRequest(curlJson)) {
return Object.assign(httpNodeParameters, {
contentType: 'binaryData',
sendBody: true,
});
}
if (contentType && !SUPPORTED_CONTENT_TYPES.includes(contentType)) {
return Object.assign(httpNodeParameters, {
sendBody: true,
contentType: 'raw',
rawContentType: contentType,
body: Object.keys(curlJson?.data ?? {})[0],
});
}
if (isJsonRequest(curlJson)) {
Object.assign(httpNodeParameters, {
contentType: 'json',
sendBody: true,
});
if (curlJson.data) {
const json = extractJson(curlJson.data);
if (jsonHasNestedObjects(json)) {
// json body
Object.assign(httpNodeParameters, {
specifyBody: 'json',
jsonBody: JSON.stringify(json, null, 2),
});
} else {
// key-value body
Object.assign(httpNodeParameters, {
specifyBody: 'keypair',
bodyParameters: {
parameters: jsonBodyToNodeParameters(curlJson.data),
},
});
}
}
} else if (isFormUrlEncodedRequest(curlJson)) {
Object.assign(httpNodeParameters, {
contentType: 'form-urlencoded',
sendBody: true,
specifyBody: 'keypair',
bodyParameters: {
parameters: keyValueBodyToNodeParameters(curlJson.data),
},
});
} else if (isMultipartRequest(curlJson)) {
Object.assign(httpNodeParameters, {
contentType: 'multipart-form-data',
sendBody: true,
bodyParameters: {
parameters: multipartToNodeParameters(curlJson.data, curlJson.files),
},
});
} else {
// could not figure the content type so do not set the body
Object.assign(httpNodeParameters, {
sendBody: false,
});
}
if (!Object.keys(httpNodeParameters.options?.redirect.redirect).length) {
// @ts-ignore
delete httpNodeParameters.options.redirect;
}
if (!Object.keys(httpNodeParameters.options.response.response).length) {
// @ts-ignore
delete httpNodeParameters.options.response;
}
return httpNodeParameters;
}
}

View file

@ -56,6 +56,7 @@
"change-case": "^5.4.4",
"chart.js": "^4.4.0",
"codemirror-lang-html-n8n": "^1.0.0",
"curlconverter": "^4.11.0",
"comlink": "^4.4.1",
"core-js": "^3.40.0",
"dateformat": "^3.0.3",
@ -89,6 +90,7 @@
"vue-router": "catalog:frontend",
"vue-virtual-scroller": "2.0.0-beta.8",
"vue3-touch-events": "^4.1.3",
"web-tree-sitter": "0.24.3",
"vuedraggable": "4.1.0",
"xss": "catalog:"
},
@ -115,6 +117,7 @@
"unplugin-icons": "^0.19.0",
"unplugin-vue-components": "^0.27.2",
"vite": "catalog:frontend",
"vite-plugin-static-copy": "2.2.0",
"vite-svg-loader": "5.1.0",
"vitest": "catalog:frontend",
"vitest-mock-extended": "catalog:frontend",

View file

@ -1,9 +0,0 @@
import type { CurlToJSONResponse, IRestApiContext } from '@/Interface';
import { makeRestApiRequest } from '@/utils/apiUtils';
export async function getCurlToJson(
context: IRestApiContext,
curlCommand: string,
): Promise<CurlToJSONResponse> {
return await makeRestApiRequest(context, 'POST', '/curl/to-json', { curlCommand });
}

View file

@ -6,7 +6,6 @@ import { useUIStore } from '@/stores/ui.store';
import { createEventBus } from 'n8n-design-system/utils';
import { useTelemetry } from '@/composables/useTelemetry';
import { useI18n } from '@/composables/useI18n';
import { useImportCurlCommand } from '@/composables/useImportCurlCommand';
const telemetry = useTelemetry();
const i18n = useI18n();
@ -18,12 +17,6 @@ const modalBus = createEventBus();
const inputRef = ref<HTMLTextAreaElement | null>(null);
const { importCurlCommand } = useImportCurlCommand({
onImportSuccess,
onImportFailure,
onAfterImport,
});
onMounted(() => {
curlCommand.value = (uiStore.modalsById[IMPORT_CURL_MODAL_KEY].data?.curlCommand as string) ?? '';
@ -71,7 +64,13 @@ function sendTelemetry(
}
async function onImport() {
await importCurlCommand(curlCommand);
const { useImportCurlCommand } = await import('@/composables/useImportCurlCommand');
const { importCurlCommand } = useImportCurlCommand({
onImportSuccess,
onImportFailure,
onAfterImport,
});
importCurlCommand(curlCommand);
}
</script>

View file

@ -0,0 +1,296 @@
import { toHttpNodeParameters } from '@/composables/useImportCurlCommand';
describe('useImportCurlCommand', () => {
describe('toHttpNodeParameters', () => {
test('Should parse form-urlencoded content type correctly', () => {
const curl =
'curl -X POST https://reqbin.com/echo/post/form -H "Content-Type: application/x-www-form-urlencoded" -d "param1=value1&param2=value2"';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo/post/form');
expect(parameters.sendBody).toBe(true);
expect(parameters.bodyParameters?.parameters[0].name).toBe('param1');
expect(parameters.bodyParameters?.parameters[0].value).toBe('value1');
expect(parameters.bodyParameters?.parameters[1].name).toBe('param2');
expect(parameters.bodyParameters?.parameters[1].value).toBe('value2');
expect(parameters.contentType).toBe('form-urlencoded');
expect(parameters.sendHeaders).toBe(false);
expect(parameters.sendQuery).toBe(false);
});
test('Should parse JSON content type correctly', () => {
const curl =
'curl -X POST https://reqbin.com/echo/post/json -H \'Content-Type: application/json\' -d \'{"login":"my_login","password":"my_password"}\'';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo/post/json');
expect(parameters.sendBody).toBe(true);
expect(parameters.bodyParameters?.parameters[0].name).toBe('login');
expect(parameters.bodyParameters?.parameters[0].value).toBe('my_login');
expect(parameters.bodyParameters?.parameters[1].name).toBe('password');
expect(parameters.bodyParameters?.parameters[1].value).toBe('my_password');
expect(parameters.contentType).toBe('json');
expect(parameters.sendHeaders).toBe(false);
expect(parameters.sendQuery).toBe(false);
});
test('Should parse multipart-form-data content type correctly', () => {
const curl =
'curl -X POST https://reqbin.com/echo/post/json -v -F key1=value1 -F upload=@localfilename';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo/post/json');
expect(parameters.sendBody).toBe(true);
expect(parameters.bodyParameters?.parameters[0].parameterType).toBe('formData');
expect(parameters.bodyParameters?.parameters[0].name).toBe('key1');
expect(parameters.bodyParameters?.parameters[0].value).toBe('value1');
expect(parameters.bodyParameters?.parameters[1].parameterType).toBe('formBinaryData');
expect(parameters.bodyParameters?.parameters[1].name).toBe('upload');
expect(parameters.contentType).toBe('multipart-form-data');
expect(parameters.sendHeaders).toBe(false);
expect(parameters.sendQuery).toBe(false);
});
test('Should parse binary request correctly', () => {
const curl =
"curl --location --request POST 'https://www.website.com' --header 'Content-Type: image/png' --data-binary '@/Users/image.png'";
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://www.website.com');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(true);
expect(parameters.contentType).toBe('binaryData');
expect(parameters.sendHeaders).toBe(false);
expect(parameters.sendQuery).toBe(false);
});
test('Should parse unknown content type correctly', () => {
const curl =
'curl -X POST https://reqbin.com/echo/post/xml -H "Content-Type: application/xml" -H "Accept: application/xml" -d "<Request><Login>my_login</Login><Password>my_password</Password></Request>"';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo/post/xml');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(true);
expect(parameters.contentType).toBe('raw');
expect(parameters.rawContentType).toBe('application/xml');
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('Accept');
expect(parameters.headerParameters?.parameters[0].value).toBe('application/xml');
expect(parameters.sendQuery).toBe(false);
});
test('Should parse header properties and keep the original case', () => {
const curl =
'curl -X POST https://reqbin.com/echo/post/json -v -F key1=value1 -F upload=@localfilename -H "ACCEPT: text/javascript" -H "content-type: multipart/form-data"';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo/post/json');
expect(parameters.sendBody).toBe(true);
expect(parameters.bodyParameters?.parameters[0].parameterType).toBe('formData');
expect(parameters.bodyParameters?.parameters[0].name).toBe('key1');
expect(parameters.bodyParameters?.parameters[0].value).toBe('value1');
expect(parameters.bodyParameters?.parameters[1].parameterType).toBe('formBinaryData');
expect(parameters.bodyParameters?.parameters[1].name).toBe('upload');
expect(parameters.contentType).toBe('multipart-form-data');
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('ACCEPT');
expect(parameters.headerParameters?.parameters[0].value).toBe('text/javascript');
expect(parameters.sendQuery).toBe(false);
});
test('Should parse querystring properties', () => {
const curl = "curl -G -d 'q=kitties' -d 'count=20' https://google.com/search";
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://google.com/search');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendHeaders).toBe(false);
expect(parameters.sendQuery).toBe(true);
expect(parameters.queryParameters?.parameters[0].name).toBe('q');
expect(parameters.queryParameters?.parameters[0].value).toBe('kitties');
expect(parameters.queryParameters?.parameters[1].name).toBe('count');
expect(parameters.queryParameters?.parameters[1].value).toBe('20');
});
test('Should parse basic authentication property and keep the original case', () => {
const curl = 'curl https://reqbin.com/echo -u "login:password"';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
});
test('Should parse location flag with --location', () => {
const curl = 'curl https://reqbin.com/echo -u "login:password" --location';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
expect(parameters.options.redirect.redirect.followRedirects).toBe(true);
});
test('Should parse location flag with --L', () => {
const curl = 'curl https://reqbin.com/echo -u "login:password" -L';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
expect(parameters.options.redirect.redirect.followRedirects).toBe(true);
});
test('Should parse location and max redirects flags with --location and --max-redirs 10', () => {
const curl = 'curl https://reqbin.com/echo -u "login:password" --location --max-redirs 10';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
expect(parameters.options.redirect.redirect.followRedirects).toBe(true);
expect(parameters.options.redirect.redirect.maxRedirects).toBe(10);
});
test('Should parse proxy flag -x', () => {
const curl = 'curl https://reqbin.com/echo -u "login:password" -x https://google.com';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
expect(parameters.options.proxy).toBe('https://google.com');
});
test('Should parse proxy flag --proxy', () => {
const curl = 'curl https://reqbin.com/echo -u "login:password" -x https://google.com';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
expect(parameters.options.proxy).toBe('https://google.com');
});
test('Should parse include headers on output flag --include', () => {
const curl =
'curl https://reqbin.com/echo -u "login:password" --include -x https://google.com';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
expect(parameters.options.response.response.fullResponse).toBe(true);
});
test('Should parse include headers on output flag -i', () => {
const curl = 'curl https://reqbin.com/echo -u "login:password" -x https://google.com -i';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.sendBody).toBe(false);
expect(parameters.contentType).toBeUndefined();
expect(parameters.sendQuery).toBe(false);
expect(parameters.sendHeaders).toBe(true);
expect(parameters.headerParameters?.parameters[0].name).toBe('authorization');
expect(parameters.headerParameters?.parameters[0].value).toBe(
`Basic ${Buffer.from('login:password').toString('base64')}`,
);
expect(parameters.options.response.response.fullResponse).toBe(true);
});
test('Should parse include request flag -X', () => {
const curl = 'curl -X POST https://reqbin.com/echo -u "login:password" -x https://google.com';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
});
test('Should parse include request flag --request', () => {
const curl =
'curl --request POST https://reqbin.com/echo -u "login:password" -x https://google.com';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
});
test('Should parse include timeout flag --connect-timeout', () => {
const curl =
'curl --request POST https://reqbin.com/echo -u "login:password" --connect-timeout 20';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
expect(parameters.options.timeout).toBe(20000);
});
test('Should parse download file flag --output', () => {
const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" --output data';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
expect(parameters.options.response.response.responseFormat).toBe('file');
expect(parameters.options.response.response.outputPropertyName).toBe('data');
});
test('Should parse download file flag -o', () => {
const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" -o data';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
expect(parameters.options.response.response.responseFormat).toBe('file');
expect(parameters.options.response.response.outputPropertyName).toBe('data');
});
test('Should parse ignore SSL flag -k', () => {
const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" -k';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
expect(parameters.options.allowUnauthorizedCerts).toBe(true);
});
test('Should parse ignore SSL flag --insecure', () => {
const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" --insecure';
const parameters = toHttpNodeParameters(curl);
expect(parameters.url).toBe('https://reqbin.com/echo');
expect(parameters.method).toBe('POST');
expect(parameters.sendBody).toBe(false);
expect(parameters.options.allowUnauthorizedCerts).toBe(true);
});
});
});

View file

@ -1,11 +1,362 @@
import type { MaybeRef } from 'vue';
import { unref } from 'vue';
import get from 'lodash/get';
import { toJsonObject as curlToJson, type JSONOutput } from 'curlconverter';
import { CURL_IMPORT_NODES_PROTOCOLS, CURL_IMPORT_NOT_SUPPORTED_PROTOCOLS } from '@/constants';
import { useToast } from '@/composables/useToast';
import { useUIStore } from '@/stores/ui.store';
import { useI18n } from '@/composables/useI18n';
import { importCurlEventBus } from '@/event-bus';
import type { BaseTextKey } from '@/plugins/i18n';
import { assert } from '@/utils/assert';
import type { CurlToJSONResponse } from '@/Interface';
interface Parameter {
parameterType?: string;
name: string;
value: string;
}
interface HttpNodeParameters extends Record<string, unknown> {
url?: string;
method: string;
sendBody?: boolean;
authentication: string;
contentType?: 'form-urlencoded' | 'multipart-form-data' | 'json' | 'raw' | 'binaryData';
rawContentType?: string;
specifyBody?: 'json' | 'keypair';
bodyParameters?: {
parameters: Parameter[];
};
jsonBody?: object;
options: {
allowUnauthorizedCerts?: boolean;
proxy?: string;
timeout?: number;
redirect: {
redirect: {
followRedirects?: boolean;
maxRedirects?: number;
};
};
response: {
response: {
fullResponse?: boolean;
responseFormat?: string;
outputPropertyName?: string;
};
};
};
sendHeaders?: boolean;
headerParameters?: {
parameters: Parameter[];
};
sendQuery?: boolean;
queryParameters?: {
parameters: Parameter[];
};
}
type HttpNodeHeaders = Pick<HttpNodeParameters, 'sendHeaders' | 'headerParameters'>;
type HttpNodeQueries = Pick<HttpNodeParameters, 'sendQuery' | 'queryParameters'>;
const SUPPORTED_CONTENT_TYPES = [
'application/json',
'application/x-www-form-urlencoded',
'multipart/form-data',
] as const;
type ContentTypes = (typeof SUPPORTED_CONTENT_TYPES)[number];
const CONTENT_TYPE_KEY = 'content-type';
const isContentType = (headers: JSONOutput['headers'], contentType: ContentTypes): boolean => {
return get(headers, CONTENT_TYPE_KEY) === contentType;
};
const isJsonRequest = (curlJson: JSONOutput): boolean => {
if (isContentType(curlJson.headers, 'application/json')) return true;
if (curlJson.data) {
const bodyKey = Object.keys(curlJson.data)[0];
try {
JSON.parse(bodyKey);
return true;
} catch {
return false;
}
}
return false;
};
const isFormUrlEncodedRequest = (curlJson: JSONOutput): boolean => {
if (isContentType(curlJson.headers, 'application/x-www-form-urlencoded')) return true;
if (curlJson.data && !curlJson.files) return true;
return false;
};
const isMultipartRequest = (curlJson: JSONOutput): boolean => {
if (isContentType(curlJson.headers, 'multipart/form-data')) return true;
if (curlJson.files)
// only multipart/form-data request include files
return true;
return false;
};
const isBinaryRequest = (curlJson: JSONOutput): boolean => {
if (curlJson?.headers?.[CONTENT_TYPE_KEY]) {
const contentType = curlJson?.headers?.[CONTENT_TYPE_KEY];
return ['image', 'video', 'audio'].some((d) => contentType.includes(d));
}
return false;
};
const toKeyValueArray = ([key, value]: [string, unknown]) => ({
name: key,
value: value?.toString() ?? '',
});
const extractHeaders = (headers: JSONOutput['headers'] = {}): HttpNodeHeaders => {
const emptyHeaders = !Object.keys(headers).length;
const onlyContentTypeHeaderDefined =
Object.keys(headers).length === 1 && headers[CONTENT_TYPE_KEY] !== undefined;
if (emptyHeaders || onlyContentTypeHeaderDefined) return { sendHeaders: false };
return {
sendHeaders: true,
headerParameters: {
parameters: Object.entries(headers)
.map(toKeyValueArray)
.filter((parameter) => parameter.name !== CONTENT_TYPE_KEY),
},
};
};
const extractQueries = (queries: JSONOutput['queries'] = {}): HttpNodeQueries => {
const emptyQueries = !Object.keys(queries).length;
if (emptyQueries) return { sendQuery: false };
return {
sendQuery: true,
queryParameters: {
parameters: Object.entries(queries).map(toKeyValueArray),
},
};
};
const jsonBodyToNodeParameters = (body: JSONOutput['data'] = {}): Parameter[] | [] => {
return Object.entries(body).map(toKeyValueArray);
};
const multipartToNodeParameters = (
body: JSONOutput['data'] = {},
files: JSONOutput['files'] = {},
): Parameter[] | [] => {
return [
...Object.entries(body)
.map(toKeyValueArray)
.map((e) => ({ parameterType: 'formData', ...e })),
...Object.entries(files)
.map(toKeyValueArray)
.map((e) => ({ parameterType: 'formBinaryData', ...e })),
];
};
const keyValueBodyToNodeParameters = (body: JSONOutput['data'] = {}): Parameter[] | [] => {
return Object.entries(body).map(toKeyValueArray);
};
const lowerCaseContentTypeKey = (obj: JSONOutput['headers']): void => {
if (!obj) return;
const regex = new RegExp(CONTENT_TYPE_KEY, 'gi');
const contentTypeKey = Object.keys(obj).find((key) => !!Array.from(key.matchAll(regex)).length);
if (!contentTypeKey) return;
const value = obj[contentTypeKey];
delete obj[contentTypeKey];
obj[CONTENT_TYPE_KEY] = value;
};
const encodeBasicAuthentication = (username: string, password: string) =>
btoa(`${username}:${password}`);
const jsonHasNestedObjects = (json: { [key: string]: string | number | object }) =>
Object.values(json).some((e) => typeof e === 'object');
const mapCookies = (cookies: JSONOutput['cookies']): { cookie: string } | {} => {
if (!cookies) return {};
const cookiesValues = Object.entries(cookies).reduce(
(accumulator: string, entry: [string, string]) => {
accumulator += `${entry[0]}=${entry[1]};`;
return accumulator;
},
'',
);
if (!cookiesValues) return {};
return {
cookie: cookiesValues,
};
};
export const flattenObject = <T extends Record<string, unknown>>(obj: T, prefix = '') =>
Object.keys(obj).reduce(
(acc, k) => {
const pre = prefix.length ? prefix + '.' : '';
if (typeof obj[k] === 'object') Object.assign(acc, flattenObject(obj[k] as T, pre + k));
else acc[pre + k] = obj[k];
return acc;
},
{} as Record<string, unknown>,
);
export const toHttpNodeParameters = (curlCommand: string): HttpNodeParameters => {
const curlJson = curlToJson(curlCommand);
const headers = curlJson.headers ?? {};
lowerCaseContentTypeKey(headers);
// set basic authentication
if (curlJson.auth) {
const { user, password: pass } = curlJson.auth;
headers.authorization = `Basic ${encodeBasicAuthentication(user, pass)}`;
}
const httpNodeParameters: HttpNodeParameters = {
url: curlJson.url,
authentication: 'none',
method: curlJson.method.toUpperCase(),
...extractHeaders({ ...headers, ...mapCookies(curlJson.cookies) }),
...extractQueries(curlJson.queries),
options: {
redirect: {
redirect: {},
},
response: {
response: {},
},
},
};
if (curlJson.follow_redirects) {
httpNodeParameters.options.redirect.redirect.followRedirects = true;
if (curlJson.max_redirects) {
httpNodeParameters.options.redirect.redirect.maxRedirects = curlJson.max_redirects;
}
}
if (curlJson.proxy) {
httpNodeParameters.options.proxy = curlJson.proxy;
}
if (curlJson.connect_timeout !== undefined) {
httpNodeParameters.options.timeout = Math.floor(curlJson.connect_timeout * 1000);
}
if (curlJson.output) {
httpNodeParameters.options.response.response = {
responseFormat: 'file',
outputPropertyName: curlJson.output ?? 'data',
};
}
if (curlJson.insecure !== undefined) {
httpNodeParameters.options.allowUnauthorizedCerts = true;
}
if (curlJson.include) {
httpNodeParameters.options.response.response.fullResponse = true;
httpNodeParameters.options.response.response.responseFormat = 'autodetect';
}
const contentType = curlJson?.headers?.[CONTENT_TYPE_KEY] as ContentTypes;
if (isBinaryRequest(curlJson)) {
return Object.assign(httpNodeParameters, {
contentType: 'binaryData',
sendBody: true,
});
}
if (contentType && !SUPPORTED_CONTENT_TYPES.includes(contentType)) {
return Object.assign(httpNodeParameters, {
sendBody: true,
contentType: 'raw',
rawContentType: contentType,
body: Object.keys(curlJson?.data ?? {})[0],
});
}
if (isJsonRequest(curlJson)) {
Object.assign(httpNodeParameters, {
contentType: 'json',
sendBody: true,
});
if (curlJson.data) {
const json = curlJson.data;
if (jsonHasNestedObjects(json)) {
// json body
Object.assign(httpNodeParameters, {
specifyBody: 'json',
jsonBody: JSON.stringify(json, null, 2),
});
} else {
// key-value body
Object.assign(httpNodeParameters, {
specifyBody: 'keypair',
bodyParameters: {
parameters: jsonBodyToNodeParameters(curlJson.data),
},
});
}
}
} else if (isFormUrlEncodedRequest(curlJson)) {
Object.assign(httpNodeParameters, {
contentType: 'form-urlencoded',
sendBody: true,
specifyBody: 'keypair',
bodyParameters: {
parameters: keyValueBodyToNodeParameters(curlJson.data),
},
});
} else if (isMultipartRequest(curlJson)) {
Object.assign(httpNodeParameters, {
contentType: 'multipart-form-data',
sendBody: true,
bodyParameters: {
parameters: multipartToNodeParameters(curlJson.data, curlJson.files),
},
});
} else {
// could not figure the content type so do not set the body
Object.assign(httpNodeParameters, {
sendBody: false,
});
}
if (!Object.keys(httpNodeParameters.options?.redirect.redirect).length) {
// @ts-ignore
delete httpNodeParameters.options.redirect;
}
if (!Object.keys(httpNodeParameters.options.response.response).length) {
// @ts-ignore
delete httpNodeParameters.options.response;
}
return httpNodeParameters;
};
export function useImportCurlCommand(options?: {
onImportSuccess?: () => void;
@ -18,7 +369,6 @@ export function useImportCurlCommand(options?: {
};
};
}) {
const uiStore = useUIStore();
const toast = useToast();
const i18n = useI18n();
@ -30,20 +380,28 @@ export function useImportCurlCommand(options?: {
...options?.i18n,
};
async function importCurlCommand(curlCommandRef: MaybeRef<string>): Promise<void> {
function importCurlCommand(curlCommandRef: MaybeRef<string>): void {
const curlCommand = unref(curlCommandRef);
if (curlCommand === '') return;
try {
const parameters = await uiStore.getCurlToJson(curlCommand);
const url = parameters['parameters.url'];
const parameters = flattenObject(toHttpNodeParameters(curlCommand), 'parameters');
assert(typeof parameters['parameters.url'] === 'string', 'parameters.url has to be string');
// Normalize placeholder values
const url: string = parameters['parameters.url']
.replaceAll('%7B', '{')
.replaceAll('%7D', '}');
const invalidProtocol = CURL_IMPORT_NOT_SUPPORTED_PROTOCOLS.find((p) =>
url.includes(`${p}://`),
);
if (!invalidProtocol) {
importCurlEventBus.emit('setHttpNodeParameters', parameters);
parameters['parameters.url'] = url;
importCurlEventBus.emit(
'setHttpNodeParameters',
parameters as unknown as CurlToJSONResponse,
);
options?.onImportSuccess?.();

View file

@ -51,7 +51,6 @@ import type {
} from '@/Interface';
import { defineStore } from 'pinia';
import { useRootStore } from '@/stores/root.store';
import * as curlParserApi from '@/api/curlHelper';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store';
@ -524,19 +523,6 @@ export const useUIStore = defineStore(STORES.UI, () => {
sidebarMenuCollapsed.value = newCollapsedState;
};
const getCurlToJson = async (curlCommand: string) => {
const parameters = await curlParserApi.getCurlToJson(rootStore.restApiContext, curlCommand);
// Normalize placeholder values
if (parameters['parameters.url']) {
parameters['parameters.url'] = parameters['parameters.url']
.replaceAll('%7B', '{')
.replaceAll('%7D', '}');
}
return parameters;
};
const removeBannerFromStack = (name: BannerName) => {
bannerStack.value = bannerStack.value.filter((bannerName) => bannerName !== name);
};
@ -653,7 +639,6 @@ export const useUIStore = defineStore(STORES.UI, () => {
resetSelectedNodes,
setCurlCommand,
toggleSidebarMenuCollapse,
getCurlToJson,
removeBannerFromStack,
dismissBanner,
updateBannersHeight,

View file

@ -1,6 +1,7 @@
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
import { defineConfig, mergeConfig } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import svgLoader from 'vite-svg-loader';
import { vitestConfig } from '@n8n/frontend-vitest-config';
@ -63,6 +64,12 @@ const plugins = [
}),
],
}),
viteStaticCopy({
targets: [
{ src: resolve('node_modules/web-tree-sitter/tree-sitter.wasm'), dest: '' },
{ src: resolve('node_modules/curlconverter/dist/tree-sitter-bash.wasm'), dest: '' },
],
}),
vue(),
svgLoader(),
legacy({
@ -73,6 +80,7 @@ const plugins = [
];
const { RELEASE: release } = process.env;
const target = browserslistToEsbuild(browsers);
export default mergeConfig(
defineConfig({
@ -100,7 +108,12 @@ export default mergeConfig(
build: {
minify: !!release,
sourcemap: !!release,
target: browserslistToEsbuild(browsers),
target,
},
optimizeDeps: {
esbuildOptions: {
target,
},
},
worker: {
format: 'es',

File diff suppressed because one or more lines are too long

View file

@ -151,6 +151,9 @@ patchedDependencies:
bull@4.12.1:
hash: ep6h4rqtpclldfcdohxlgcb3aq
path: patches/bull@4.12.1.patch
curlconverter@4.11.0:
hash: ymigioonidbyw7jydnlor7bohu
path: patches/curlconverter@4.11.0.patch
pkce-challenge@3.0.0:
hash: dypouzb3lve7vncq25i5fuanki
path: patches/pkce-challenge@3.0.0.patch
@ -868,9 +871,6 @@ importers:
csrf:
specifier: 3.1.0
version: 3.1.0
curlconverter:
specifier: 3.21.0
version: 3.21.0(chokidar@4.0.1)
dotenv:
specifier: 8.6.0
version: 8.6.0
@ -1516,6 +1516,9 @@ importers:
core-js:
specifier: ^3.40.0
version: 3.40.0
curlconverter:
specifier: ^4.11.0
version: 4.11.0(patch_hash=ymigioonidbyw7jydnlor7bohu)
dateformat:
specifier: ^3.0.3
version: 3.0.3
@ -1612,6 +1615,9 @@ importers:
vuedraggable:
specifier: 4.1.0
version: 4.1.0(vue@3.5.13(typescript@5.7.2))
web-tree-sitter:
specifier: 0.24.3
version: 0.24.3
xss:
specifier: 'catalog:'
version: 1.0.15
@ -1682,6 +1688,9 @@ importers:
vite:
specifier: catalog:frontend
version: 6.0.2(@types/node@18.16.16)(jiti@1.21.0)(sass@1.64.1)(terser@5.16.1)
vite-plugin-static-copy:
specifier: 2.2.0
version: 2.2.0(vite@6.0.2(@types/node@18.16.16)(jiti@1.21.0)(sass@1.64.1)(terser@5.16.1))
vite-svg-loader:
specifier: 5.1.0
version: 5.1.0(vue@3.5.13(typescript@5.7.2))
@ -3304,14 +3313,6 @@ packages:
resolution: {integrity: sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==}
engines: {node: '>=10'}
'@curlconverter/yargs-parser@0.0.1':
resolution: {integrity: sha512-DbEVRYqrorzwqc63MQ3RODflut1tNla8ZCKo1h83lF7+fbntgubZsDfRDBv5Lxj3vkKuvAolysNM2ekwJev8wA==}
engines: {node: '>=10'}
'@curlconverter/yargs@0.0.2':
resolution: {integrity: sha512-Q1YEebpCY61kxme4wvU0/IN/uMBfG5pZOKCo9FU+w20ElPvN+eH2qEVbK1C12t3Tee3qeYLLEU6HkiUeO1gc4A==}
engines: {node: '>=12'}
'@cypress/request@3.0.1':
resolution: {integrity: sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==}
engines: {node: '>= 6'}
@ -6492,9 +6493,6 @@ packages:
engines: {node: '>=10.0.0'}
deprecated: this version has critical issues, please update to the latest version
a-sync-waterfall@1.0.1:
resolution: {integrity: sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==}
abab@2.0.6:
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
deprecated: Use your platform's native atob() and btoa() methods instead
@ -6685,9 +6683,6 @@ packages:
aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
array-buffer-byte-length@1.0.0:
resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==}
array-buffer-byte-length@1.0.1:
resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==}
engines: {node: '>= 0.4'}
@ -6728,10 +6723,6 @@ packages:
resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==}
engines: {node: '>= 0.4'}
arraybuffer.prototype.slice@1.0.1:
resolution: {integrity: sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==}
engines: {node: '>= 0.4'}
arraybuffer.prototype.slice@1.0.3:
resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==}
engines: {node: '>= 0.4'}
@ -7358,10 +7349,6 @@ packages:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
commander@5.1.0:
resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
engines: {node: '>= 6'}
commander@6.2.1:
resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
engines: {node: '>= 6'}
@ -7464,10 +7451,6 @@ packages:
cookie-signature@1.0.6:
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
cookie@0.4.2:
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
engines: {node: '>= 0.6'}
cookie@0.7.1:
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
engines: {node: '>= 0.6'}
@ -7605,8 +7588,8 @@ packages:
csv-parse@5.5.0:
resolution: {integrity: sha512-RxruSK3M4XgzcD7Trm2wEN+SJ26ChIb903+IWxNOcB5q4jT2Cs+hFr6QP39J05EohshRFEvyzEBoZ/466S2sbw==}
curlconverter@3.21.0:
resolution: {integrity: sha512-DXCnp1A/Xa69FujksUfdvWQFAnIn/C+4Wuv8t+UVdZkF/lY5bzj98GGKOGme7V/ckSHDLxE29Xp76sJ5Cpsp5A==}
curlconverter@4.11.0:
resolution: {integrity: sha512-jBSvfDN10L6rGWVlkAYgtkIG8lYprDvtBgos7mafxtv15keYeQWsxUgnzns3JmqEcGJMeaGlDNdRUszURPCUaw==}
hasBin: true
currency-codes@2.1.0:
@ -7780,10 +7763,6 @@ packages:
decko@1.2.0:
resolution: {integrity: sha512-m8FnyHXV1QX+S1cl+KPFDIl6NMkxtKsy6+U/aYyjrOqWMuwAwYWu7ePqrsUHtDR5Y8Yk2pi/KIDSgF+vT4cPOQ==}
decode-uri-component@0.2.2:
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
engines: {node: '>=0.10'}
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
@ -8106,10 +8085,6 @@ packages:
error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
es-abstract@1.22.1:
resolution: {integrity: sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==}
engines: {node: '>= 0.4'}
es-abstract@1.22.5:
resolution: {integrity: sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w==}
engines: {node: '>= 0.4'}
@ -8143,10 +8118,6 @@ packages:
resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.0.1:
resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.0.3:
resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==}
engines: {node: '>= 0.4'}
@ -8595,10 +8566,6 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
filter-obj@1.1.0:
resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==}
engines: {node: '>=0.10.0'}
finalhandler@1.3.1:
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
engines: {node: '>= 0.8'}
@ -8707,6 +8674,10 @@ packages:
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
fs-extra@11.3.0:
resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==}
engines: {node: '>=14.14'}
fs-extra@7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'}
@ -8735,10 +8706,6 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
function.prototype.name@1.1.5:
resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==}
engines: {node: '>= 0.4'}
function.prototype.name@1.1.6:
resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==}
engines: {node: '>= 0.4'}
@ -8809,10 +8776,6 @@ packages:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
get-symbol-description@1.0.0:
resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==}
engines: {node: '>= 0.4'}
get-symbol-description@1.0.2:
resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==}
engines: {node: '>= 0.4'}
@ -9197,10 +9160,6 @@ packages:
resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==}
engines: {node: '>=8.0.0'}
internal-slot@1.0.5:
resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==}
engines: {node: '>= 0.4'}
internal-slot@1.0.7:
resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
engines: {node: '>= 0.4'}
@ -9232,9 +9191,6 @@ packages:
resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
engines: {node: '>= 0.4'}
is-array-buffer@3.0.2:
resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==}
is-array-buffer@3.0.4:
resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==}
engines: {node: '>= 0.4'}
@ -9323,10 +9279,6 @@ packages:
resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==}
engines: {node: '>= 0.4'}
is-negative-zero@2.0.2:
resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
engines: {node: '>= 0.4'}
is-negative-zero@2.0.3:
resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
engines: {node: '>= 0.4'}
@ -9370,9 +9322,6 @@ packages:
is-set@2.0.2:
resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==}
is-shared-array-buffer@1.0.2:
resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==}
is-shared-array-buffer@1.0.3:
resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==}
engines: {node: '>= 0.4'}
@ -10153,6 +10102,9 @@ packages:
lossless-json@1.0.5:
resolution: {integrity: sha512-RicKUuLwZVNZ6ZdJHgIZnSeA05p8qWc5NW0uR96mpPIjN9WDLUg9+kj1esQU1GkPn9iLZVKatSQK5gyiaFHgJA==}
lossless-json@4.0.2:
resolution: {integrity: sha512-+z0EaLi2UcWi8MZRxA5iTb6m4Ys4E80uftGY+yG5KNFJb5EceQXOhdW/pWJZ8m97s26u7yZZAYMcKWNztSZssA==}
loupe@3.1.2:
resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==}
@ -10741,6 +10693,10 @@ packages:
resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==}
engines: {node: ^16 || ^18 || >= 20}
node-addon-api@8.3.0:
resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==}
engines: {node: ^18 || ^20 || >= 21}
node-cleanup@2.1.2:
resolution: {integrity: sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==}
@ -10781,6 +10737,10 @@ packages:
resolution: {integrity: sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==}
hasBin: true
node-gyp-build@4.8.4:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true
node-gyp@8.4.1:
resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==}
engines: {node: '>= 10.12.0'}
@ -10879,16 +10839,6 @@ packages:
number-allocator@1.0.14:
resolution: {integrity: sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==}
nunjucks@3.2.4:
resolution: {integrity: sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==}
engines: {node: '>= 6.9.0'}
hasBin: true
peerDependencies:
chokidar: ^4.0.1
peerDependenciesMeta:
chokidar:
optional: true
nwsapi@2.2.7:
resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==}
@ -11650,10 +11600,6 @@ packages:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
query-string@7.1.3:
resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
engines: {node: '>=6'}
querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
@ -11846,10 +11792,6 @@ packages:
resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
hasBin: true
regexp.prototype.flags@1.5.0:
resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==}
engines: {node: '>= 0.4'}
regexp.prototype.flags@1.5.2:
resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==}
engines: {node: '>= 0.4'}
@ -12030,10 +11972,6 @@ packages:
rxjs@7.8.1:
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
safe-array-concat@1.0.0:
resolution: {integrity: sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==}
engines: {node: '>=0.4'}
safe-array-concat@1.1.2:
resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==}
engines: {node: '>=0.4'}
@ -12044,9 +11982,6 @@ packages:
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-regex-test@1.0.0:
resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==}
safe-regex-test@1.0.3:
resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==}
engines: {node: '>= 0.4'}
@ -12315,10 +12250,6 @@ packages:
resolution: {integrity: sha512-VNiXjFp6R4ldPbVRYbpxlD35yRHceecVXlct1J4/X80KuuPnW2AXMq3sGwhnJOhKkUsOxAT6nRGfGE5pocVw5w==}
engines: {node: '>=10.0.0'}
split-on-first@1.1.0:
resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
engines: {node: '>=6'}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
@ -12432,10 +12363,6 @@ packages:
strict-event-emitter@0.5.1:
resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==}
strict-uri-encode@2.0.0:
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
engines: {node: '>=4'}
string-argv@0.3.1:
resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==}
engines: {node: '>=0.6.19'}
@ -12452,13 +12379,6 @@ packages:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
string.prototype.startswith@1.0.0:
resolution: {integrity: sha512-VHhsDkuf8gsw4JNRK9cIZjYe6r7PsVUutVohaBhqYAoPaRADoQH+mMgUg7Cs/TgQeDGEvI+PzPEMOdvdsCMvpg==}
string.prototype.trim@1.2.7:
resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==}
engines: {node: '>= 0.4'}
string.prototype.trim@1.2.8:
resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==}
engines: {node: '>= 0.4'}
@ -12467,18 +12387,12 @@ packages:
resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==}
engines: {node: '>= 0.4'}
string.prototype.trimend@1.0.6:
resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==}
string.prototype.trimend@1.0.7:
resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==}
string.prototype.trimend@1.0.8:
resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==}
string.prototype.trimstart@1.0.6:
resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==}
string.prototype.trimstart@1.0.7:
resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==}
@ -12794,6 +12708,17 @@ packages:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
tree-sitter-bash@0.23.3:
resolution: {integrity: sha512-36cg/GQ2YmIbeiBeqeuh4fBJ6i4kgVouDaqTxqih5ysPag+zHufyIaxMOFeM8CeplwAK/Luj1o5XHqgdAfoCZg==}
peerDependencies:
tree-sitter: ^0.21.1
peerDependenciesMeta:
tree-sitter:
optional: true
tree-sitter@0.21.1:
resolution: {integrity: sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==}
triple-beam@1.3.0:
resolution: {integrity: sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==}
@ -13014,33 +12939,18 @@ packages:
resolution: {integrity: sha512-SOnx8xygcAh8lvDU2exnK2bomASfNjzB3Qz71s2tw9QnX8fkAo7aC+D0H7FV0HjRKj94CKV2Hi71kVkkO6nOxg==}
engines: {node: '>=0.10.5'}
typed-array-buffer@1.0.0:
resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==}
engines: {node: '>= 0.4'}
typed-array-buffer@1.0.2:
resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==}
engines: {node: '>= 0.4'}
typed-array-byte-length@1.0.0:
resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==}
engines: {node: '>= 0.4'}
typed-array-byte-length@1.0.1:
resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==}
engines: {node: '>= 0.4'}
typed-array-byte-offset@1.0.0:
resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==}
engines: {node: '>= 0.4'}
typed-array-byte-offset@1.0.2:
resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==}
engines: {node: '>= 0.4'}
typed-array-length@1.0.4:
resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==}
typed-array-length@1.0.5:
resolution: {integrity: sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==}
engines: {node: '>= 0.4'}
@ -13300,6 +13210,12 @@ packages:
vite:
optional: true
vite-plugin-static-copy@2.2.0:
resolution: {integrity: sha512-ytMrKdR9iWEYHbUxs6x53m+MRl4SJsOSoMu1U1+Pfg0DjPeMlsRVx3RR5jvoonineDquIue83Oq69JvNsFSU5w==}
engines: {node: ^18.0.0 || >=20.0.0}
peerDependencies:
vite: ^5.0.0 || ^6.0.0
vite-svg-loader@5.1.0:
resolution: {integrity: sha512-M/wqwtOEjgb956/+m5ZrYT/Iq6Hax0OakWbokj8+9PXOnB7b/4AxESHieEtnNEy7ZpjsjYW1/5nK8fATQMmRxw==}
peerDependencies:
@ -13536,6 +13452,9 @@ packages:
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
engines: {node: '>= 14'}
web-tree-sitter@0.24.3:
resolution: {integrity: sha512-uR9YNewr1S2EzPKE+y39nAwaTyobBaZRG/IsfkB/OT4v0lXtNj5WjtHKgn2h7eOYUWIZh5rK9Px7tI6S9CRKdA==}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@ -15938,18 +15857,6 @@ snapshots:
'@ctrl/tinycolor@3.6.0': {}
'@curlconverter/yargs-parser@0.0.1': {}
'@curlconverter/yargs@0.0.2':
dependencies:
'@curlconverter/yargs-parser': 0.0.1
cliui: 7.0.4
escalade: 3.1.1
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
'@cypress/request@3.0.1':
dependencies:
aws-sign2: 0.7.0
@ -19664,8 +19571,6 @@ snapshots:
'@xmldom/xmldom@0.8.6': {}
a-sync-waterfall@1.0.1: {}
abab@2.0.6: {}
abbrev@1.1.1: {}
@ -19852,11 +19757,6 @@ snapshots:
dependencies:
dequal: 2.0.3
array-buffer-byte-length@1.0.0:
dependencies:
call-bind: 1.0.7
is-array-buffer: 3.0.2
array-buffer-byte-length@1.0.1:
dependencies:
call-bind: 1.0.7
@ -19914,15 +19814,6 @@ snapshots:
es-abstract: 1.22.5
es-shim-unscopables: 1.0.0
arraybuffer.prototype.slice@1.0.1:
dependencies:
array-buffer-byte-length: 1.0.0
call-bind: 1.0.7
define-properties: 1.2.1
get-intrinsic: 1.2.4
is-array-buffer: 3.0.2
is-shared-array-buffer: 1.0.2
arraybuffer.prototype.slice@1.0.3:
dependencies:
array-buffer-byte-length: 1.0.1
@ -20677,8 +20568,6 @@ snapshots:
commander@4.1.1: {}
commander@5.1.0: {}
commander@6.2.1: {}
commander@7.2.0: {}
@ -20788,8 +20677,6 @@ snapshots:
cookie-signature@1.0.6: {}
cookie@0.4.2: {}
cookie@0.7.1: {}
cookie@0.7.2: {}
@ -20933,17 +20820,14 @@ snapshots:
csv-parse@5.5.0: {}
curlconverter@3.21.0(chokidar@4.0.1):
curlconverter@4.11.0(patch_hash=ymigioonidbyw7jydnlor7bohu):
dependencies:
'@curlconverter/yargs': 0.0.2
cookie: 0.4.2
jsesc: 3.0.2
nunjucks: 3.2.4(chokidar@4.0.1)
query-string: 7.1.3
string.prototype.startswith: 1.0.0
lossless-json: 4.0.2
tree-sitter: 0.21.1
tree-sitter-bash: 0.23.3(tree-sitter@0.21.1)
web-tree-sitter: 0.24.3
yamljs: 0.3.0
transitivePeerDependencies:
- chokidar
currency-codes@2.1.0:
dependencies:
@ -21136,8 +21020,6 @@ snapshots:
decko@1.2.0: {}
decode-uri-component@0.2.2: {}
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
@ -21486,48 +21368,6 @@ snapshots:
dependencies:
is-arrayish: 0.2.1
es-abstract@1.22.1:
dependencies:
array-buffer-byte-length: 1.0.0
arraybuffer.prototype.slice: 1.0.1
available-typed-arrays: 1.0.7
call-bind: 1.0.7
es-set-tostringtag: 2.0.1
es-to-primitive: 1.2.1
function.prototype.name: 1.1.5
get-intrinsic: 1.2.4
get-symbol-description: 1.0.0
globalthis: 1.0.3
gopd: 1.0.1
has: 1.0.3
has-property-descriptors: 1.0.2
has-proto: 1.0.3
has-symbols: 1.0.3
internal-slot: 1.0.5
is-array-buffer: 3.0.2
is-callable: 1.2.7
is-negative-zero: 2.0.2
is-regex: 1.1.4
is-shared-array-buffer: 1.0.2
is-string: 1.0.7
is-typed-array: 1.1.13
is-weakref: 1.0.2
object-inspect: 1.13.1
object-keys: 1.1.1
object.assign: 4.1.5
regexp.prototype.flags: 1.5.0
safe-array-concat: 1.0.0
safe-regex-test: 1.0.0
string.prototype.trim: 1.2.7
string.prototype.trimend: 1.0.6
string.prototype.trimstart: 1.0.6
typed-array-buffer: 1.0.0
typed-array-byte-length: 1.0.0
typed-array-byte-offset: 1.0.0
typed-array-length: 1.0.4
unbox-primitive: 1.0.2
which-typed-array: 1.1.15
es-abstract@1.22.5:
dependencies:
array-buffer-byte-length: 1.0.1
@ -21658,12 +21498,6 @@ snapshots:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.0.1:
dependencies:
get-intrinsic: 1.2.4
has: 1.0.3
has-tostringtag: 1.0.2
es-set-tostringtag@2.0.3:
dependencies:
get-intrinsic: 1.2.4
@ -22265,8 +22099,6 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
filter-obj@1.1.0: {}
finalhandler@1.3.1:
dependencies:
debug: 2.6.9
@ -22378,6 +22210,12 @@ snapshots:
fs-constants@1.0.0: {}
fs-extra@11.3.0:
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.1.0
universalify: 2.0.0
fs-extra@7.0.1:
dependencies:
graceful-fs: 4.2.11
@ -22405,13 +22243,6 @@ snapshots:
function-bind@1.1.2: {}
function.prototype.name@1.1.5:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.22.5
functions-have-names: 1.2.3
function.prototype.name@1.1.6:
dependencies:
call-bind: 1.0.7
@ -22505,11 +22336,6 @@ snapshots:
get-stream@6.0.1: {}
get-symbol-description@1.0.0:
dependencies:
call-bind: 1.0.7
get-intrinsic: 1.2.4
get-symbol-description@1.0.2:
dependencies:
call-bind: 1.0.7
@ -23020,12 +22846,6 @@ snapshots:
strip-ansi: 6.0.1
through: 2.3.8
internal-slot@1.0.5:
dependencies:
get-intrinsic: 1.2.4
has: 1.0.3
side-channel: 1.0.6
internal-slot@1.0.7:
dependencies:
es-errors: 1.3.0
@ -23071,12 +22891,6 @@ snapshots:
call-bind: 1.0.7
has-tostringtag: 1.0.2
is-array-buffer@3.0.2:
dependencies:
call-bind: 1.0.7
get-intrinsic: 1.2.4
is-typed-array: 1.1.13
is-array-buffer@3.0.4:
dependencies:
call-bind: 1.0.7
@ -23157,8 +22971,6 @@ snapshots:
call-bind: 1.0.7
define-properties: 1.2.1
is-negative-zero@2.0.2: {}
is-negative-zero@2.0.3: {}
is-node-process@1.2.0: {}
@ -23188,10 +23000,6 @@ snapshots:
is-set@2.0.2: {}
is-shared-array-buffer@1.0.2:
dependencies:
call-bind: 1.0.7
is-shared-array-buffer@1.0.3:
dependencies:
call-bind: 1.0.7
@ -24195,6 +24003,8 @@ snapshots:
lossless-json@1.0.5: {}
lossless-json@4.0.2: {}
loupe@3.1.2: {}
lower-case@1.1.4: {}
@ -24996,6 +24806,8 @@ snapshots:
node-addon-api@7.1.0: {}
node-addon-api@8.3.0: {}
node-cleanup@2.1.2: {}
node-domexception@1.0.0: {}
@ -25023,6 +24835,8 @@ snapshots:
node-gyp-build-optional-packages@5.0.7:
optional: true
node-gyp-build@4.8.4: {}
node-gyp@8.4.1:
dependencies:
env-paths: 2.2.1
@ -25153,14 +24967,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
nunjucks@3.2.4(chokidar@4.0.1):
dependencies:
a-sync-waterfall: 1.0.1
asap: 2.0.6
commander: 5.1.0
optionalDependencies:
chokidar: 4.0.1
nwsapi@2.2.7: {}
oas-kit-common@1.0.8:
@ -25959,13 +25765,6 @@ snapshots:
dependencies:
side-channel: 1.0.6
query-string@7.1.3:
dependencies:
decode-uri-component: 0.2.2
filter-obj: 1.1.0
split-on-first: 1.1.0
strict-uri-encode: 2.0.0
querystringify@2.2.0: {}
queue-lit@1.5.0: {}
@ -26227,12 +26026,6 @@ snapshots:
regexp-tree@0.1.27: {}
regexp.prototype.flags@1.5.0:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
functions-have-names: 1.2.3
regexp.prototype.flags@1.5.2:
dependencies:
call-bind: 1.0.7
@ -26430,13 +26223,6 @@ snapshots:
dependencies:
tslib: 2.6.2
safe-array-concat@1.0.0:
dependencies:
call-bind: 1.0.7
get-intrinsic: 1.2.4
has-symbols: 1.0.3
isarray: 2.0.5
safe-array-concat@1.1.2:
dependencies:
call-bind: 1.0.7
@ -26448,12 +26234,6 @@ snapshots:
safe-buffer@5.2.1: {}
safe-regex-test@1.0.0:
dependencies:
call-bind: 1.0.7
get-intrinsic: 1.2.4
is-regex: 1.1.4
safe-regex-test@1.0.3:
dependencies:
call-bind: 1.0.7
@ -26844,8 +26624,6 @@ snapshots:
spex@3.3.0: {}
split-on-first@1.1.0: {}
split2@4.2.0: {}
split@0.3.3:
@ -26976,8 +26754,6 @@ snapshots:
strict-event-emitter@0.5.1: {}
strict-uri-encode@2.0.0: {}
string-argv@0.3.1: {}
string-length@4.0.2:
@ -26997,17 +26773,6 @@ snapshots:
emoji-regex: 9.2.2
strip-ansi: 7.1.0
string.prototype.startswith@1.0.0:
dependencies:
define-properties: 1.2.0
es-abstract: 1.22.1
string.prototype.trim@1.2.7:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.22.5
string.prototype.trim@1.2.8:
dependencies:
call-bind: 1.0.7
@ -27021,12 +26786,6 @@ snapshots:
es-abstract: 1.23.3
es-object-atoms: 1.0.0
string.prototype.trimend@1.0.6:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.22.5
string.prototype.trimend@1.0.7:
dependencies:
call-bind: 1.0.7
@ -27039,12 +26798,6 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.0.0
string.prototype.trimstart@1.0.6:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.22.5
string.prototype.trimstart@1.0.7:
dependencies:
call-bind: 1.0.7
@ -27423,6 +27176,18 @@ snapshots:
tree-kill@1.2.2: {}
tree-sitter-bash@0.23.3(tree-sitter@0.21.1):
dependencies:
node-addon-api: 8.3.0
node-gyp-build: 4.8.4
optionalDependencies:
tree-sitter: 0.21.1
tree-sitter@0.21.1:
dependencies:
node-addon-api: 8.3.0
node-gyp-build: 4.8.4
triple-beam@1.3.0: {}
ts-api-utils@1.0.1(typescript@5.7.2):
@ -27620,25 +27385,12 @@ snapshots:
type-of-is@3.5.1: {}
typed-array-buffer@1.0.0:
dependencies:
call-bind: 1.0.7
get-intrinsic: 1.2.4
is-typed-array: 1.1.13
typed-array-buffer@1.0.2:
dependencies:
call-bind: 1.0.7
es-errors: 1.3.0
is-typed-array: 1.1.13
typed-array-byte-length@1.0.0:
dependencies:
call-bind: 1.0.7
for-each: 0.3.3
has-proto: 1.0.3
is-typed-array: 1.1.13
typed-array-byte-length@1.0.1:
dependencies:
call-bind: 1.0.7
@ -27647,14 +27399,6 @@ snapshots:
has-proto: 1.0.3
is-typed-array: 1.1.13
typed-array-byte-offset@1.0.0:
dependencies:
available-typed-arrays: 1.0.7
call-bind: 1.0.7
for-each: 0.3.3
has-proto: 1.0.3
is-typed-array: 1.1.13
typed-array-byte-offset@1.0.2:
dependencies:
available-typed-arrays: 1.0.7
@ -27664,12 +27408,6 @@ snapshots:
has-proto: 1.0.3
is-typed-array: 1.1.13
typed-array-length@1.0.4:
dependencies:
call-bind: 1.0.7
for-each: 0.3.3
is-typed-array: 1.1.13
typed-array-length@1.0.5:
dependencies:
call-bind: 1.0.7
@ -27935,6 +27673,14 @@ snapshots:
- rollup
- supports-color
vite-plugin-static-copy@2.2.0(vite@6.0.2(@types/node@18.16.16)(jiti@1.21.0)(sass@1.64.1)(terser@5.16.1)):
dependencies:
chokidar: 4.0.1
fast-glob: 3.3.2
fs-extra: 11.3.0
picocolors: 1.1.1
vite: 6.0.2(@types/node@18.16.16)(jiti@1.21.0)(sass@1.64.1)(terser@5.16.1)
vite-svg-loader@5.1.0(vue@3.5.13(typescript@5.7.2)):
dependencies:
svgo: 3.3.2
@ -28179,6 +27925,8 @@ snapshots:
web-streams-polyfill@4.0.0-beta.3: {}
web-tree-sitter@0.24.3: {}
webidl-conversions@3.0.1: {}
webidl-conversions@4.0.2: {}