Compare commits

..

48 Commits

Author SHA1 Message Date
Anoop M D
00e98451d4 Merge branch 'main' into fix/json-serialization-issues 2024-08-27 14:09:10 +05:30
lohit
b4fd350334 feat: electron sandbox bundle libraries check (#2932)
chore: check sandbox libraries bundle file
2024-08-27 14:05:49 +05:30
Mateusz Pietryga
ad388e5a6a fix: Failed to construct 'URL': Invalid URL everytime the URL is modified (#2897)
* fix: Failed to construct 'URL': Invalid URL everytime the URL is modified

A non-parsable URL should be an acceptable state while the text is being typed.

* chore: path params are returned as empty

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-27 12:19:04 +05:30
Pragadesh-45
4ddccefee3 fix: multiple tab collection level settings (#2905)
* added isFolderSettingsOpenedInTabs logic
2024-08-27 11:51:19 +05:30
lohit
b121afe7bb feat: revert await keyword for test (#2933)
feat: revert await keyword for test
2024-08-27 11:41:09 +05:30
LeoGrambert
f6c6a3b2bf feat: add Georgian README translations (#2931) 2024-08-27 11:04:34 +05:30
Timon
cf02ea2572 feat: Replace dictonary i18n with react-i18next (#2915) 2024-08-26 11:09:50 +05:30
zachary-berdell-elliott
9343f1e070 Bugfix/request body value cutoff (#2917)
* Changed display: contents to display: block to fix bug with text cutoff in multipart form and form url encoded request bodies.

* Changed display: contents to display: block to fix bug with text cutoff in multipart form and form url encoded request bodies.
2024-08-26 11:06:33 +05:30
Anoop M D
d000625c39 release: v1.26.2 2024-08-26 00:47:39 +05:30
lohit
4bdbfb5c0c fix: safe mode validations (#2912) 2024-08-24 14:29:43 +05:30
Jan Monschke
4fbd2f0bdb Self-host Inter font to fix GDPR issue (#750)
* chore: self-host Inter

fixes https://github.com/usebruno/bruno/issues/695

* chore: fix package-lock to include fontsource

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-24 02:24:24 +05:30
Anoop M D
5ae3f0e75a fix: revert pr #1121 - back to jsonlint 2024-08-24 02:13:13 +05:30
Sanjeev Shrestha
b80269b68f Remove deprecated jsonlint package and use upto date package (#1121)
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-24 02:05:22 +05:30
Bruno Braga
67de396927 feature: add search results count to CodeMirror (#1498) 2024-08-24 01:49:38 +05:30
Anoop M D
65b80cfd06 chore: fix lint issue 2024-08-24 01:40:09 +05:30
Nikhil569
caa0a22e74 Shorten method name for OPTIONS and DELETE (#588) 2024-08-24 01:30:19 +05:30
Rinku Chaudhari
c7f0335d96 fix: response headers count logic update (#1488) 2024-08-24 01:25:49 +05:30
chrisn
22fab7f599 Update bruno-cli/options-description (#1592)
- Addition to the parameter description for the new tests-only switch

Co-authored-by: Chris Nagel <mail@chrisnagel.de>
2024-08-24 01:19:47 +05:30
Hinnerk Oetting
c0d214f2bc Add documentation to postman import and export (#2274)
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-24 01:12:31 +05:30
shawnsarwar
9efbd7377a feat: support relative links in markdown for docs (#2375)
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-24 01:01:55 +05:30
Michel Descoteaux
b63cf46734 feat: Add grapqhl in export to postman collection (#2386) 2024-08-24 00:52:15 +05:30
Miguel Hernández
73ac969d35 fix: replace curly braces with colons in OpenAPI path parameters (#2513) (#2533) 2024-08-24 00:49:37 +05:30
tlaloc911
dc21206fc0 fix pre text style (#2545) 2024-08-24 00:45:57 +05:30
Santiago Chiabotto
8f58235e17 added tailwind pointer-events classes to query result filter (#2535) 2024-08-24 00:44:12 +05:30
François Mockers
fd6b3630a5 extract basic OpenAPI links (#2624) 2024-08-24 00:41:45 +05:30
Zhaolin Liang
25e57d2578 bugfix/fix save text response as base64 (#2886) 2024-08-24 00:38:21 +05:30
Niklas Ziermann
cef6f85845 feature(#2839): introduce singular dev script to improve DX (#2840)
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-24 00:34:23 +05:30
lohit
5f0e6f13eb fix: quickjs vm handle errors (#2906)
* fix: quickjs vm handle errors
2024-08-23 18:16:43 +05:30
Pragadesh-45
0b9554c8cc Fix/rename-collection-support-wsl (#2892)
* fix: normalize wsl path for rename item

* added isWSLPath, normalizeWslPath

* revert normalize action on actions.js

* added WSL path checking and apply Win UNC normalize
2024-08-23 16:36:54 +05:30
anusreesubash
8b76ecede3 test: added test for self closing tags in xml-json parser (#2891)
Co-authored-by: Anusree Subash <anusree@usebruno.com>
2024-08-23 16:29:41 +05:30
lohit
44d70ca02a fix: boolean, undefined, null values eval in quickjs vm (#2893)
fix: boolean, undeifned, null values in pre-request vars
2024-08-23 16:18:41 +05:30
lohit
4d55b50250 fix: interpolation of multiple interdependent variables (#2899)
fix: interpolation with multiple interdependent variables in input string
2024-08-23 16:13:00 +05:30
Anoop M D
6320a80cbe fix: fix failing tests caused by upgrading to node v20.15.0 2024-08-22 21:04:38 +05:30
Mateusz Pietryga
d1c34bd379 Feat/ Update node version used by project to match Electron (#2673)
* Feat/electron-bump - bump node version to match electron

* Feat/electron-bump - bump node version - documentation

* Feat/electron-bump - bump node version - package-lock.json
2024-08-22 18:13:29 +05:30
Mateusz Pietryga
71ffe1f8d4 Fix: update rollup / fix build warnings (#2711) 2024-08-22 18:12:40 +05:30
Anoop M D
04ccb2f6ee chore: made path param hint easier on the eyes and ux 2024-08-22 18:11:35 +05:30
Anoop M D
99ddd8021c chore: made path param hint easier on the eyes and ux 2024-08-22 17:58:27 +05:30
Krystian Marcisz
ee8e162f3d feat: add hint for Request Path Variables to improve UX (#2873)
* feat: add hint for Request Path Variables to improve UX
2024-08-22 17:40:39 +05:30
Anoop M D
b1a140a4e0 release: v1.26.1 2024-08-22 13:52:38 +05:30
lohit
e4407f3981 chore: updated testbench - primitive data types (#2888)
* fix: getRequestVar shim, boolean values in vm
* chore: updated testbench
2024-08-22 11:37:13 +05:30
Anoop M D
74e75a7da2 fix: fixed ux issues around content indicator being big 2024-08-22 11:33:35 +05:30
lohit
4aff61b665 fix: getRequestVar shim, boolean values in vm (#2887) 2024-08-22 10:10:15 +05:30
lohit
df2e18bedd bru disableParsingResponseJson function rename (#2785)
* disable response json parse flag

* fix: pr review comments

* update bru req function name
2024-08-08 19:07:12 +05:30
Anoop M D
9a88db7e56 fix(#2767): fix failing test 2024-08-08 18:46:35 +05:30
lohit
4a4439f48e disable response json parse flag (#2782)
* disable response json parse flag

* fix: pr review comments
2024-08-08 18:36:00 +05:30
Anoop M D
4710928407 fix(#2767): addressing review comments 2024-08-08 12:21:04 +05:30
Anoop M D
33804f4c7b fix(#2767): addressing review comments 2024-08-08 12:02:00 +05:30
Anoop M D
e0858d1c99 fix(#2767): Fix serilization issues of bigint in json body 2024-08-07 20:00:41 +05:30
106 changed files with 3380 additions and 4394 deletions

2
.nvmrc
View File

@@ -1 +1 @@
v20.9.0
v20.9.0

View File

@@ -34,10 +34,11 @@ Libraries we use
- Schema Validation - Yup
- Request Client - axios
- Filesystem Watcher - chokidar
- i18n - i18next
### Dependencies
You would need [Node v18.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
You would need [Node v20.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
## Development

View File

@@ -37,7 +37,7 @@ Bruno 基于 NextJs 和 React 构建。我们使用 Electron 来封装桌面版
### 依赖项
您需要 [Node v18.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我们在这个项目中也使用 npm 工作区_npm workspaces_
您需要 [Node v20.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我们在这个项目中也使用 npm 工作区_npm workspaces_
## 开发

View File

@@ -37,7 +37,7 @@ Bibliotheken die wir benutzen
### Abhängigkeiten
Du benötigst [Node v18.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
Du benötigst [Node v20.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
### Lass uns coden

View File

@@ -37,7 +37,7 @@ Librerías que utilizamos:
### Dependencias
Necesitarás [Node v18.x o la última versión LTS](https://nodejs.org/es) y npm 8.x. Ten en cuenta que utilizamos espacios de trabajo de npm en el proyecto.
Necesitarás [Node v20.x o la última versión LTS](https://nodejs.org/es) y npm 8.x. Ten en cuenta que utilizamos espacios de trabajo de npm en el proyecto.
## Desarrollo

View File

@@ -37,7 +37,7 @@ Les librairies que nous utilisons :
### Dépendances
Vous aurez besoin de [Node v18.x ou la dernière version LTS](https://nodejs.org/en/) et npm 8.x. Nous utilisons aussi les espaces de travail npm (_npm workspaces_) dans ce projet.
Vous aurez besoin de [Node v20.x ou la dernière version LTS](https://nodejs.org/en/) et npm 8.x. Nous utilisons aussi les espaces de travail npm (_npm workspaces_) dans ce projet.
## Développement

View File

@@ -37,7 +37,7 @@ Libraries जिनका हम उपयोग करते हैं
### निर्भरताएँ
आपको [Node v18.x या नवीनतम LTS संस्करण](https://nodejs.org/en/) और npm 8.x की आवश्यकता होगी। हम प्रोजेक्ट में npm वर्कस्पेस का उपयोग करते हैं
आपको [Node v20.x या नवीनतम LTS संस्करण](https://nodejs.org/en/) और npm 8.x की आवश्यकता होगी। हम प्रोजेक्ट में npm वर्कस्पेस का उपयोग करते हैं
## डेवलपमेंट

View File

@@ -37,7 +37,7 @@ Le librerie che utilizziamo sono:
### Dependences
Hai bisogno di [Node v18.x o dell'ultima versione LTS](https://nodejs.org/en/) di npm 8.x. Utilizziamo gli spazi di lavoro npm (_npm workspaces_) in questo progetto.
Hai bisogno di [Node v20.x o dell'ultima versione LTS](https://nodejs.org/en/) di npm 8.x. Utilizziamo gli spazi di lavoro npm (_npm workspaces_) in questo progetto.
### Iniziamo a codificare

View File

@@ -37,7 +37,7 @@ Bruno は Next.js と React で作られています。デスクトップアプ
### 依存関係
[Node v18.x もしくは最新の LTS バージョン](https://nodejs.org/en/)と npm 8.x が必要です。プロジェクトに npm ワークスペースを使用しています。
[Node v20.x もしくは最新の LTS バージョン](https://nodejs.org/en/)と npm 8.x が必要です。プロジェクトに npm ワークスペースを使用しています。
## 開発

View File

@@ -37,7 +37,7 @@ Bruno는 Next.js와 React로 구축되었습니다. 또한, (로컬 컬렉션을
### 의존성
[Node v18.x 혹은 최신 LTS version](https://nodejs.org/en/)과 npm 8.x 버전이 필요합니다. 우리는 이 프로젝트에서 npm workspaces를 사용합니다.
[Node v20.x 혹은 최신 LTS version](https://nodejs.org/en/)과 npm 8.x 버전이 필요합니다. 우리는 이 프로젝트에서 npm workspaces를 사용합니다.
## 개발

View File

@@ -37,7 +37,7 @@ Biblioteki, których używamy
### Zależności
Będziesz potrzebować [Node v18.x lub najnowszej wersji LTS](https://nodejs.org/en/) oraz npm 8.x. W projekcie używamy npm workspaces
Będziesz potrzebować [Node v20.x lub najnowszej wersji LTS](https://nodejs.org/en/) oraz npm 8.x. W projekcie używamy npm workspaces
## Rozwój

View File

@@ -37,7 +37,7 @@ Bibliotecas que utilizamos:
### Dependências
Você precisará do [Node v18.x (ou da versão LTS mais recente)](https://nodejs.org/en/) e do npm na versão 8.x. Nós utilizamos npm workspaces no projeto.
Você precisará do [Node v20.x (ou da versão LTS mais recente)](https://nodejs.org/en/) e do npm na versão 8.x. Nós utilizamos npm workspaces no projeto.
## Desenvolvimento

View File

@@ -37,7 +37,7 @@ Bibliotecile pe care le folosim
### Dependențele
Veți avea nevoie de [Node v18.x sau cea mai recentă versiune LTS](https://nodejs.org/en/) și npm 8.x. Noi folosim spații de lucru npm în proiect
Veți avea nevoie de [Node v20.x sau cea mai recentă versiune LTS](https://nodejs.org/en/) și npm 8.x. Noi folosim spații de lucru npm în proiect
## Dezvoltarea

View File

@@ -37,7 +37,7 @@ Bruno построен с использованием Next.js и React. Мы т
### Зависимости
Вам потребуется [Node v18.x или последняя версия LTS](https://nodejs.org/en/) и npm 8.x. В проекте мы используем рабочие пространства npm
Вам потребуется [Node v20.x или последняя версия LTS](https://nodejs.org/en/) и npm 8.x. В проекте мы используем рабочие пространства npm
### Приступим к коду

View File

@@ -37,7 +37,7 @@ Kullandığımız kütüphaneler
### Bağımlılıklar
[Node v18.x veya en son LTS sürümüne](https://nodejs.org/en/) ve npm 8.x'e ihtiyacınız olacaktır. Projede npm çalışma alanlarını kullanıyoruz
[Node v20.x veya en son LTS sürümüne](https://nodejs.org/en/) ve npm 8.x'e ihtiyacınız olacaktır. Projede npm çalışma alanlarını kullanıyoruz
## Gelişim

View File

@@ -37,7 +37,7 @@ Bruno побудований на Next.js та React. Також для деск
### Залежності
Вам знадобиться [Node v18.x або остання LTS версія](https://nodejs.org/en/) та npm 8.x. Ми використовуєм npm workspaces в цьому проекті
Вам знадобиться [Node v20.x або остання LTS версія](https://nodejs.org/en/) та npm 8.x. Ми використовуєм npm workspaces в цьому проекті
### Починаєм писати код

View File

@@ -37,7 +37,7 @@ Bruno 使用 Next.js 和 React 構建。我們使用 Electron 來封裝及發佈
### 依賴關係
您需要使用 [Node v18.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我們在這個專案中使用 npm 工作區_npm workspaces_
您需要使用 [Node v20.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我們在這個專案中使用 npm 工作區_npm workspaces_
## 開發

View File

@@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md)
| **العربية**
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
برونو هو عميل API جديد ومبتكر، يهدف إلى ثورة الحالة الحالية التي يمثلها برنامج Postman وأدوات مماثلة هناك.

View File

@@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
ব্রুনো হল একটি নতুন এবং উদ্ভাবনী API ক্লায়েন্ট, যার লক্ষ্য পোস্টম্যান এবং অনুরূপ সরঞ্জাম দ্বারা প্রতিনিধিত্ব করা স্থিতাবস্থায় বিপ্লব ঘটানো।

View File

@@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno 是一款全新且创新的 API 客户端,旨在颠覆 Postman 和其他类似工具。

View File

@@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno ist ein neuer und innovativer API-Client, der den Status Quo von Postman und ähnlichen Tools revolutionieren soll.

View File

@@ -27,6 +27,8 @@
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno es un cliente de APIs nuevo e innovador, creado con el objetivo de revolucionar el panorama actual representado por Postman y otras herramientas similares.
Bruno almacena tus colecciones directamente en una carpeta de tu sistema de archivos. Usamos un lenguaje de marcado de texto plano, llamado Bru, para guardar información sobre las peticiones a tus APIs.

View File

@@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno est un nouveau client API, innovant, qui a pour but de révolutionner le _statu quo_ que représentent Postman et les autres outils.

View File

@@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno è un nuovo ed innovativo API client, mirato a rivoluzionare lo status quo rappresentato da Postman e strumenti simili disponibili.

View File

@@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| **日本語**
| [ქართული](./readme_ka.md)
Bruno は革新的な API クライアントです。Postman を代表する API クライアントツールの現状に一石を投じることを目指しています。

176
docs/readme/readme_ka.md Normal file
View File

@@ -0,0 +1,176 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### ბრუნო - ღია წყაროების IDE API-ების შესწავლისა და ტესტირებისათვის.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](../../readme.md)
| [Українська](./readme_ua.md)
| [Русский](./readme_ru.md)
| [Türkçe](./readme_tr.md)
| [Deutsch](./readme_de.md)
| [Français](./readme_fr.md)
| [Português (BR)](./readme_pt_br.md)
| [한국어](./readme_kr.md)
| [বাংলা](./readme_bn.md)
| [Español](./readme_es.md)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| **ქართული**
ბრუნო არის ახალი და ინოვაციური API კლიენტი, რომელიც მიზნად ისახავს პოსტმანისა და მსგავსი ინსტრუმენტების არსებული მდგომარეობის რევოლუციას.
ბრუნო თქვენი კოლექციების შენახვას უშუალოდ თქვენს ფაილური სისტემის ერთ-ერთ საქაღალოში ახდენს. ჩვენ ვხმარობთ უბრალო ტექსტურ მარკაპ ენის, Bru-ს, API მოთხოვნების შესახებ ინფორმაციის შენახვისთვის.
თქვენ შეგიძლიათ გამოიყენოთ Git ან ნებისმიერი ვერსიის კონტროლის სისტემა თქვენი API კოლექციების გასაზიარებლად.
ბრუნო მხოლოდ ოფლაინ რეჟიმში მუშაობს. ბრუნოში ღრუბლური სინქრონიზაციის დამატების გეგმები არ არის. ჩვენ ვაფასებთ თქვენი მონაცემების პრივატობას და creemos, რომ ისინი თქვენს მოწყობილობაში უნდა დარჩეს. წაიკითხეთ ჩვენი გრძელვადიანი ხედვა [აქ](https://github.com/usebruno/bruno/discussions/269)
[დამატებით ბრუნო](https://www.usebruno.com/downloads)
📢 შეიტყვეთ ჩვენი უახლესი საუბრის შესახებ India FOSS 3.0 კონფერენციაზე [აქ](https://www.youtube.com/watch?v=7bSMFpbcPiY)
![bruno](../../assets/images/landing-2.png) <br /><br />
### ოქროს გამოცემა ✨
მთავარი ფუნქციების უმეტესობა უფასოა და ღია წყაროა. ჩვენ ვცდილობთ ჰარმონიული ბალანსის დაცვას [ღია წყაროების პრინციპებსა და მდგრადობას შორის](https://github.com/usebruno/bruno/discussions/269)
თქვენ შეგიძლიათ შეიძინოთ [ოქროს გამოცემა](https://www.usebruno.com/pricing) ერთჯერადი გადახდით **19 დოლარად**! <br/>
### ინსტალაცია
ბრუნო ხელმისაწვდომია როგორც ბინარული ჩამოტვირთვა [ჩვენ的网站上](https://www.usebruno.com/downloads) Mac-ის, Windows-ისა და Linux-ისთვის.
თქვენ ასევე შეგიძლიათ დააინსტალიროთ ბრუნო პაკეტის მენეჯერების საშუალებით, როგორიცაა Homebrew, Chocolatey, Scoop, Snap, Flatpak და Apt.
```sh
# Mac-ზე Homebrew-ს საშუალებით
brew install bruno
# Windows-ზე Chocolatey-ს საშუალებით
choco install bruno
# Windows-ზე Scoop-ის საშუალებით
scoop bucket add extras
scoop install bruno
# Windows-ზე winget-ის საშუალებით
winget install Bruno.Bruno
# Linux-ზე Snap-ის საშუალებით
snap install bruno
# Linux-ზე Flatpak-ის საშუალებით
flatpak install com.usebruno.Bruno
# Linux-ზე Apt-ის საშუალებით
sudo mkdir -p /etc/apt/keyrings
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
sudo apt update
sudo apt install bruno
```
### პლატფორმებს შორის მუშაობა 🖥️
![bruno](../../assets/images/run-anywhere.png) <br /><br />
### თანამშრომლობა Git-ის საშუალებით 👩‍💻🧑‍💻
ან ნებისმიერი ვერსიის კონტროლის სისტემის საშუალებით
![bruno](../../assets/images/version-control.png) <br /><br />
### სპონსორები
#### ოქროს სპონსორები
<img src="../../assets/images/sponsors/samagata.png" width="150"/>
#### ვერცხლის სპონსორები
<img src="../../assets/images/sponsors/commit-company.png" width="70"/>
#### ბრინჯის სპონსორები
<a href="https://zuplo.link/bruno">
<img src="../../assets/images/sponsors/zuplo.png" width="120"/>
</a>
### მნიშვნელოვანი ბმულები 📌
- [ჩვენი გრძელვადიანი ხედვა](https://github.com/usebruno/bruno/discussions/269)
- [გეგმა](https://github.com/usebruno/bruno/discussions/384)
- [დოკუმენტაცია](https://docs.usebruno.com)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
- [ვებსაიტი](https://www.usebruno.com)
- [ფასები](https://www.usebruno.com/pricing)
- [დამატება](https://www.usebruno.com/downloads)
- [GitHub სპონსორები](https://github.com/sponsors/helloanoop).
### ვიტრინა 🎥
- [მოწონებები](https://github.com/usebruno/bruno/discussions/343)
- [მეცნიერების ჰაბი](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
### მხარდაჭერა ❤️
თუ გიყვართ ბრუნო და გინდათ მხარი დაუჭიროთ ჩვენს ღია წყაროების მუშაობას, გაითვალისწინეთ ჩვენი დახმარება [GitHub სპონსორების საშუალებით](https://github.com/sponsors/helloanoop).
### გააზიარეთ მოწმობები 📣
თუ ბრუნო დაგეხმარათ თქვენს სამუშაოში და გუნდებში, გთხოვთ, არ დაგავიწყდეთ ჩვენი [მოწონებების გაზიარება ჩვენს GitHub განხილვაში](https://github.com/usebruno/bruno/discussions/343)
### ახალი პაკეტის მენეჯერებში გამოქვეყნება
იხილეთ [აქ](../../publishing.md) მეტი ინფორმაციისათვის.
### დაინტერესდით 🌐
[𝕎 (Twitter)](https://twitter.com/use_bruno) <br />
[ვებსაიტი](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](https://www.linkedin.com/company/usebruno)
### სავაჭრო ნიშანი
**სახელი**
`ბრუნო` არის სავაჭრო ნიშანი, რომელსაც ფლობს [ანუპ მ. დ.](https://www.helloanoop.com/)
**ლოგო**
ლოგო არის [OpenMoji](https://openmoji.org/library/emoji-1F436/) სურათებიდან. ლიცენზია: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### თანამშრომლობა 👩‍💻🧑‍💻
მიხარია, რომ დაინტერესებული ხართ ბრუნოს გაუმჯობესებით. გთხოვთ, გადახედეთ [თანამშრომლობის სახელმძღვანელოს](../../contributing.md)
თუნდაც ვერ მოახერხოთ კოდის საშუალებით კონტრიბუცია, ნუ ინანებთ პრობლემების და ფუნქციის მოთხოვნების ჩაწერას, რომლებიც უნდა განხორციელდეს თქვენი შემთხვევის გადასაჭრელად.
### ავტორები
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### ლიცენზია 📄
[MIT](../../license.md)

View File

@@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno는 새롭고 혁신적인 API 클라이언트로, Postman과 유사한 툴들을 혁신하는 것을 목표로 합니다.

View File

@@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno to nowy i innowacyjny klient API, którego celem jest zrewolucjonizowanie status quo reprezentowanego przez narzędzia takie jak Postman.

View File

@@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno é um novo e inovador cliente de API, com o objetivo de revolucionar o status quo representado por ferramentas como o Postman e outras semelhantes.

View File

@@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno este un client API nou și inovativ, care vizează să revoluționeze status quo-ul reprezentat de Postman și alte instrumente similare.

View File

@@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами.

View File

@@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno, Postman ve benzeri araçlar tarafından temsil edilen statükoda devrim yaratmayı amaçlayan yeni ve yenilikçi bir API istemcisidir.

View File

@@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman.

View File

@@ -27,6 +27,7 @@
| **正體中文**
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno 是一個全新且有創新性的 API 用戶端,目的在徹底改變以 Postman 和其他類似工具的現況。

6354
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,14 +20,17 @@
"@jest/globals": "^29.2.0",
"@playwright/test": "^1.27.1",
"@types/jest": "^29.5.11",
"concurrently": "^8.2.2",
"fs-extra": "^11.1.1",
"husky": "^8.0.3",
"jest": "^29.2.0",
"pretty-quick": "^3.1.3",
"randomstring": "^1.2.2",
"rimraf": "^6.0.1",
"ts-jest": "^29.0.5"
},
"scripts": {
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
"dev:web": "npm run dev --workspace=packages/bruno-app",
"build:web": "npm run build --workspace=packages/bruno-app",
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
@@ -48,7 +51,7 @@
"prepare": "husky install"
},
"overrides": {
"rollup": "3.2.5"
"rollup":"3.29.4"
},
"dependencies": {
"json-bigint": "^1.0.0",

View File

@@ -12,6 +12,7 @@
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
},
"dependencies": {
"@fontsource/inter": "^5.0.15",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.16",
@@ -35,7 +36,8 @@
"graphiql": "^1.5.9",
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
"httpsnippet": "^3.0.1",
"httpsnippet": "^3.0.6",
"i18next": "^23.14.0",
"idb": "^7.0.0",
"immer": "^9.0.15",
"jsesc": "^3.0.2",
@@ -47,6 +49,7 @@
"know-your-http-well": "^0.5.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
"mousetrap": "^1.6.5",
"nanoid": "3.3.4",
"next": "12.3.3",
@@ -64,6 +67,7 @@
"react-dom": "18.2.0",
"react-github-btn": "^1.4.0",
"react-hot-toast": "^2.4.0",
"react-i18next": "^15.0.1",
"react-inspector": "^6.0.2",
"react-pdf": "^7.5.1",
"react-redux": "^7.2.6",

View File

@@ -14,6 +14,24 @@ const StyledWrapper = styled.div`
background: #d2d7db;
}
.CodeMirror-dialog {
overflow: visible;
}
#search-results-count {
display: inline-block;
position: absolute;
top: calc(100% + 1px);
right: 0;
border-width: 0 0 1px 1px;
border-style: solid;
border-color: ${(props) => props.theme.codemirror.border};
padding: 0.1em 0.8em;
background-color: ${(props) => props.theme.codemirror.bg};
color: rgb(102, 102, 102);
white-space: nowrap;
}
textarea.cm-editor {
position: relative;
}

View File

@@ -66,7 +66,10 @@ if (!SERVER_RENDERED) {
'bru.getVar(key)',
'bru.setVar(key,value)',
'bru.deleteVar(key)',
'bru.setNextRequest(requestName)'
'bru.setNextRequest(requestName)',
'req.disableParsingResponseJson()'
'bru.getRequestVar(key)',
'bru.sleep(ms)'
];
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
const cursor = editor.getCursor();
@@ -109,6 +112,7 @@ export default class CodeEditor extends React.Component {
// unnecessary updates during the update lifecycle.
this.cachedValue = props.value || '';
this.variables = {};
this.searchResultsCountElementId = 'search-results-count';
this.lintOptions = {
esversion: 11,
@@ -155,8 +159,16 @@ export default class CodeEditor extends React.Component {
this.props.onSave();
}
},
'Cmd-F': 'findPersistent',
'Ctrl-F': 'findPersistent',
'Cmd-F': (cm) => {
cm.execCommand('findPersistent');
this._bindSearchHandler();
this._appendSearchResultsCount();
},
'Ctrl-F': (cm) => {
cm.execCommand('findPersistent');
this._bindSearchHandler();
this._appendSearchResultsCount();
},
'Cmd-H': 'replace',
'Ctrl-H': 'replace',
Tab: function (cm) {
@@ -308,6 +320,8 @@ export default class CodeEditor extends React.Component {
this.editor.off('change', this._onEdit);
this.editor = null;
}
this._unbindSearchHandler();
}
render() {
@@ -344,4 +358,62 @@ export default class CodeEditor extends React.Component {
}
}
};
/**
* Bind handler to search input to count number of search results
*/
_bindSearchHandler = () => {
const searchInput = document.querySelector('.CodeMirror-search-field');
if (searchInput) {
searchInput.addEventListener('input', this._countSearchResults);
}
};
/**
* Unbind handler to search input to count number of search results
*/
_unbindSearchHandler = () => {
const searchInput = document.querySelector('.CodeMirror-search-field');
if (searchInput) {
searchInput.removeEventListener('input', this._countSearchResults);
}
};
/**
* Append search results count to search dialog
*/
_appendSearchResultsCount = () => {
const dialog = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
if (dialog) {
const searchResultsCount = document.createElement('span');
searchResultsCount.id = this.searchResultsCountElementId;
dialog.appendChild(searchResultsCount);
this._countSearchResults();
}
};
/**
* Count search results and update state
*/
_countSearchResults = () => {
let count = 0;
const searchInput = document.querySelector('.CodeMirror-search-field');
if (searchInput && searchInput.value.length > 0) {
const text = new RegExp(searchInput.value, 'gi');
const matches = this.editor.getValue().match(text);
count = matches ? matches.length : 0;
}
const searchResultsCountElement = document.querySelector(`#${this.searchResultsCountElementId}`);
if (searchResultsCountElement) {
searchResultsCountElement.innerText = `${count} results`;
}
};
}

View File

@@ -48,7 +48,7 @@ const Docs = ({ collection }) => {
font={get(preferences, 'font.codeFont', 'default')}
/>
) : (
<Markdown onDoubleClick={toggleViewMode} content={docs} />
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
)}
</StyledWrapper>
);

View File

@@ -53,7 +53,7 @@ const Documentation = ({ item, collection }) => {
mode="application/text"
/>
) : (
<Markdown onDoubleClick={toggleViewMode} content={docs} />
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
)}
</StyledWrapper>
);

View File

@@ -19,8 +19,8 @@ const FolderSettings = ({ collection, folder }) => {
const setTab = (tab) => {
dispatch(
updatedFolderSettingsSelectedTab({
collectionUid: collection.uid,
folderUid: folder.uid,
collectionUid: collection?.uid,
folderUid: folder?.uid,
tab
})
);

View File

@@ -0,0 +1,16 @@
import React from 'react';
const DotIcon = ({ width }) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={width}
viewBox="0 0 24 24" strokeWidth="1.5"
stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round"
className='inline-block'
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor" />
</svg>
);
};
export default DotIcon;

View File

@@ -69,6 +69,7 @@ const StyledMarkdownBodyWrapper = styled.div`
pre {
background: ${(props) => props.theme.sidebar.bg};
color: ${(props) => props.theme.text};
}
table {

View File

@@ -1,10 +1,15 @@
import MarkdownIt from 'markdown-it';
import * as MarkdownItReplaceLink from 'markdown-it-replace-link';
import StyledWrapper from './StyledWrapper';
import React from 'react';
const md = new MarkdownIt();
const Markdown = ({ collectionPath, onDoubleClick, content }) => {
const markdownItOptions = {
replaceLink: function (link, env) {
return link.replace(/^\./, collectionPath);
}
};
const Markdown = ({ onDoubleClick, content }) => {
const handleOnClick = (event) => {
const target = event.target;
if (target.tagName === 'A') {
@@ -23,6 +28,8 @@ const Markdown = ({ onDoubleClick, content }) => {
}
};
const md = new MarkdownIt(markdownItOptions).use(MarkdownItReplaceLink);
const htmlFromMarkdown = md.render(content || '');
return (

View File

@@ -17,7 +17,7 @@ const StyledWrapper = styled.div`
overflow: hidden !important;
${'' /* padding-bottom: 50px !important; */}
position: relative;
display: contents;
display: block;
margin: 0px;
padding: 0px;
}

View File

@@ -1,10 +1,10 @@
import React from 'react';
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import { useDictionary } from 'providers/Dictionary/index';
import { useTranslation } from 'react-i18next';
const Support = () => {
const { dictionary } = useDictionary();
const { t } = useTranslation();
return (
<StyledWrapper>
@@ -12,31 +12,31 @@ const Support = () => {
<div className="mt-2">
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end">
<IconBook size={18} strokeWidth={2} />
<span className="label ml-2">{dictionary.documentation}</span>
<span className="label ml-2">{t('COMMON.DOCUMENTATION')}</span>
</a>
</div>
<div className="mt-2">
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end">
<IconSpeakerphone size={18} strokeWidth={2} />
<span className="label ml-2">{dictionary.reportIssues}</span>
<span className="label ml-2">{t('COMMON.REPORT_ISSUES')}</span>
</a>
</div>
<div className="mt-2">
<a href="https://discord.com/invite/KgcZUncpjq" target="_blank" className="flex items-end">
<IconBrandDiscord size={18} strokeWidth={2} />
<span className="label ml-2">{dictionary.discord}</span>
<span className="label ml-2">{t('COMMON.DISCORD')}</span>
</a>
</div>
<div className="mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end">
<IconBrandGithub size={18} strokeWidth={2} />
<span className="label ml-2">{dictionary.gitHub}</span>
<span className="label ml-2">{t('COMMON.GITHUB')}</span>
</a>
</div>
<div className="mt-2">
<a href="https://twitter.com/use_bruno" target="_blank" className="flex items-end">
<IconBrandTwitter size={18} strokeWidth={2} />
<span className="label ml-2">{dictionary.twitter}</span>
<span className="label ml-2">{t('COMMON.TWITTER')}</span>
</a>
</div>
</div>

View File

@@ -23,6 +23,10 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.tabs.active.color} !important;
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
}
.content-indicator {
color: ${(props) => props.theme.text}
}
}
}
`;

View File

@@ -7,7 +7,7 @@ import RequestHeaders from 'components/RequestPane/RequestHeaders';
import RequestBody from 'components/RequestPane/RequestBody';
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
import Auth from 'components/RequestPane/Auth';
import AuthMode from 'components/RequestPane/Auth/AuthMode';
import DotIcon from 'components/Icons/Dot';
import Vars from 'components/RequestPane/Vars';
import Assertions from 'components/RequestPane/Assertions';
import Script from 'components/RequestPane/Script';
@@ -16,7 +16,11 @@ import StyledWrapper from './StyledWrapper';
import { find, get } from 'lodash';
import Documentation from 'components/Documentation/index';
const CONTENT_INDICATOR = '\u25CF';
const ContentIndicator = () => {
return <sup className="ml-[.125rem] opacity-80 font-medium">
<DotIcon width="10"></DotIcon>
</sup>
};
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const dispatch = useDispatch();
@@ -112,11 +116,11 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
</div>
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
Body
{body.mode !== 'none' && <sup className="ml-1 font-medium">{CONTENT_INDICATOR}</sup>}
{body.mode !== 'none' && <ContentIndicator />}
</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
{activeHeadersLength > 0 && <sup className="ml-1 font-medium">{activeHeadersLength}</sup>}
{activeHeadersLength > 0 && <sup className="ml-[.125rem] font-medium">{activeHeadersLength}</sup>}
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
Auth
@@ -127,7 +131,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
Script
{(script.req || script.res) && <sup className="ml-1 font-medium">{CONTENT_INDICATOR}</sup>}
{(script.req || script.res) && <ContentIndicator />}
</div>
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
Assert
@@ -135,7 +139,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
</div>
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
Tests
{tests && <sup className="ml-1 font-medium">{CONTENT_INDICATOR}</sup>}
{tests && <ContentIndicator />}
</div>
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
Docs

View File

@@ -1,7 +1,7 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import has from 'lodash/has';
import Tooltip from 'components/Tooltip';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
@@ -103,7 +103,7 @@ const QueryParams = ({ item, collection }) => {
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Query</div>
<div className="mb-2 title text-xs">Query</div>
<table>
<thead>
<tr>
@@ -173,7 +173,22 @@ const QueryParams = ({ item, collection }) => {
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddQueryParam}>
+&nbsp;<span>Add Param</span>
</button>
<div className="mb-1 title text-xs">Path</div>
<div className="mb-2 title text-xs flex items-stretch">
<span>Path</span>
<Tooltip
text={`
<div>
Path variables are automatically added whenever the
<code className="font-mono mx-2">:name</code>
template is used in the URL. <br/> For example:
<code className="font-mono mx-2">
https://example.com/v1/users/<span>:id</span>
</code>
</div>
`}
tooltipId="path-param-tooltip"
/>
</div>
<table>
<thead>
<tr>
@@ -224,6 +239,11 @@ const QueryParams = ({ item, collection }) => {
: null}
</tbody>
</table>
{!(pathParams && pathParams.length) ?
<div className="title pr-2 py-3 mt-2 text-xs">
</div>
: null}
</div>
</StyledWrapper>
);

View File

@@ -46,7 +46,7 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
return (
<div
className={
'response-filter absolute bottom-2 w-full justify-end right-0 flex flex-row items-center gap-2 py-4 px-2'
'response-filter absolute bottom-2 w-full justify-end right-0 flex flex-row items-center gap-2 py-4 px-2 pointer-events-none'
}
>
{tooltipText && !isExpanded && <ReactTooltip anchorId={'request-filter-icon'} html={tooltipText} />}
@@ -61,11 +61,11 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
autoCapitalize="off"
spellCheck="false"
className={`block ml-14 p-2 py-1 sm:text-sm transition-all duration-200 ease-in-out border border-gray-300 rounded-md ${
isExpanded ? 'w-full opacity-100' : 'w-[0] opacity-0'
isExpanded ? 'w-full opacity-100 pointer-events-auto' : 'w-[0] opacity-0'
}`}
onChange={onChange}
/>
<div className="text-gray-500 sm:text-sm cursor-pointer" id="request-filter-icon" onClick={handleFilterClick}>
<div className="text-gray-500 sm:text-sm cursor-pointer pointer-events-auto" id="request-filter-icon" onClick={handleFilterClick}>
{isExpanded ? <IconX size={20} strokeWidth={1.5} /> : <IconFilter size={20} strokeWidth={1.5} />}
</div>
</div>

View File

@@ -17,8 +17,6 @@ const Timeline = ({ request, response }) => {
});
});
let requestData = safeStringifyJSON(request.data);
return (
<StyledWrapper className="pb-4 w-full">
<div>
@@ -33,9 +31,10 @@ const Timeline = ({ request, response }) => {
);
})}
{requestData ? (
{request.data ? (
<pre className="line request">
<span className="arrow">{'>'}</span> data {requestData}
<span className="arrow">{'>'}</span> data{' '}
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{request.data}</pre>
</pre>
) : null}
</div>

View File

@@ -97,6 +97,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
});
};
const responseHeadersCount = typeof response.headers === 'object' ? Object.entries(response.headers).length : 0;
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex flex-wrap items-center pl-3 pr-4 tabs" role="tablist">
@@ -105,7 +107,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
{response.headers?.length > 0 && <sup className="ml-1 font-medium">{response.headers.length}</sup>}
{responseHeadersCount > 0 && <sup className="ml-1 font-medium">{responseHeadersCount}</sup>}
</div>
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
Timeline

View File

@@ -6,7 +6,7 @@ import Portal from 'components/Portal';
import Modal from 'components/Modal';
import StyledWrapper from './StyledWrapper';
const JsSandboxModeModal = ({ collection, onClose }) => {
const JsSandboxModeModal = ({ collection }) => {
const dispatch = useDispatch();
const [jsSandboxMode, setJsSandboxMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe');
@@ -22,7 +22,6 @@ const JsSandboxModeModal = ({ collection, onClose }) => {
)
.then(() => {
toast.success('Sandbox mode updated successfully');
onClose();
})
.catch((err) => console.log(err) && toast.error('Failed to update sandbox mode'));
};
@@ -65,7 +64,7 @@ const JsSandboxModeModal = ({ collection, onClose }) => {
<span className='beta-tag'>BETA</span>
</label>
<p className='text-sm text-muted mt-1'>
JavaScript code is executed in a secure sandbox and cannot excess your filesystem or execute system commands.
JavaScript code is executed in a secure sandbox and cannot access your filesystem or execute system commands.
</p>
<label htmlFor="developer" className="flex flex-row gap-2 mt-6 cursor-pointer">

View File

@@ -50,7 +50,7 @@ const SecuritySettings = ({ collection }) => {
<span className='beta-tag'>BETA</span>
</label>
<p className='text-sm text-muted mt-1'>
JavaScript code is executed in a secure sandbox and cannot excess your filesystem or execute system commands.
JavaScript code is executed in a secure sandbox and cannot access your filesystem or execute system commands.
</p>
<label htmlFor="developer" className="flex flex-row gap-2 mt-6 cursor-pointer">

View File

@@ -23,7 +23,9 @@ const RequestMethod = ({ item }) => {
return (
<StyledWrapper>
<div className={getClassname(item.request.method)}>
<span className="uppercase">{item.request.method}</span>
<span className="uppercase">
{item.request.method.length > 5 ? item.request.method.substring(0, 3) : item.request.method}
</span>
</div>
</StyledWrapper>
);

View File

@@ -189,16 +189,28 @@ const CollectionItem = ({ item, collection, searchText }) => {
toast.error('URL is required');
}
};
const viewFolderSettings = () => {
dispatch(
addTab({
uid: uuid(),
collectionUid: collection.uid,
folderUid: item.uid,
type: 'folder-settings'
})
);
if (isItemAFolder(item)) {
if (itemIsOpenedInTabs(item, tabs)) {
dispatch(
focusTab({
uid: item.uid
})
);
return;
}
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
type: 'folder-settings'
})
);
return;
}
};
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));

View File

@@ -129,7 +129,7 @@ const Sidebar = () => {
Star
</GitHubButton> */}
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.26.0</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.26.2</div>
</div>
</div>
</div>

View File

@@ -13,7 +13,6 @@ const Tooltip = ({ text, tooltipId }) => {
fill="currentColor"
className="inline-block ml-2 cursor-pointer"
viewBox="0 0 16 16"
style={{ marginTop: 1 }}
>
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
import { IconBrandGithub, IconPlus, IconDownload, IconFolders, IconSpeakerphone, IconBook } from '@tabler/icons';
@@ -9,11 +10,10 @@ import CreateCollection from 'components/Sidebar/CreateCollection';
import ImportCollection from 'components/Sidebar/ImportCollection';
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
import StyledWrapper from './StyledWrapper';
import { useDictionary } from 'providers/Dictionary/index';
const Welcome = () => {
const dispatch = useDispatch();
const { dictionary } = useDictionary();
const { t } = useTranslation();
const [importedCollection, setImportedCollection] = useState(null);
const [importedTranslationLog, setImportedTranslationLog] = useState({});
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
@@ -22,7 +22,7 @@ const Welcome = () => {
const handleOpenCollection = () => {
dispatch(openCollection()).catch(
(err) => console.log(err) && toast.error(dictionary.errorWhileOpeningCollection)
(err) => console.log(err) && toast.error(t('WELCOME.COLLECTION_OPEN_ERROR'))
);
};
@@ -40,12 +40,12 @@ const Welcome = () => {
.then(() => {
setImportCollectionLocationModalOpen(false);
setImportedCollection(null);
toast.success(dictionary.collectionImportedSuccessfully);
toast.success(t('WELCOME.COLLECTION_IMPORT_SUCCESS'));
})
.catch((err) => {
setImportCollectionLocationModalOpen(false);
console.error(err);
toast.error(dictionary.errorWhileImportingCollection);
toast.error(t('WELCOME.COLLECTION_IMPORT_ERROR'));
});
};
@@ -68,45 +68,45 @@ const Welcome = () => {
<Bruno width={50} />
</div>
<div className="text-xl font-semibold select-none">bruno</div>
<div className="mt-4">{dictionary.aboutBruno}</div>
<div className="mt-4">{t('WELCOME.ABOUT_BRUNO')}</div>
<div className="uppercase font-semibold heading mt-10">{dictionary.collections}</div>
<div className="uppercase font-semibold heading mt-10">{t('COMMON.COLLECTIONS')}</div>
<div className="mt-4 flex items-center collection-options select-none">
<div className="flex items-center" onClick={() => setCreateCollectionModalOpen(true)}>
<IconPlus size={18} strokeWidth={2} />
<span className="label ml-2" id="create-collection">
{dictionary.createCollection}
{t('WELCOME.CREATE_COLLECTION')}
</span>
</div>
<div className="flex items-center ml-6" onClick={handleOpenCollection}>
<IconFolders size={18} strokeWidth={2} />
<span className="label ml-2">{dictionary.openCollection}</span>
<span className="label ml-2">{t('WELCOME.OPEN_COLLECTION')}</span>
</div>
<div className="flex items-center ml-6" onClick={() => setImportCollectionModalOpen(true)}>
<IconDownload size={18} strokeWidth={2} />
<span className="label ml-2" id="import-collection">
{dictionary.importCollection}
{t('WELCOME.IMPORT_COLLECTION')}
</span>
</div>
</div>
<div className="uppercase font-semibold heading mt-10 pt-6">Links</div>
<div className="uppercase font-semibold heading mt-10 pt-6">{t('WELCOME.LINKS')}</div>
<div className="mt-4 flex flex-col collection-options select-none">
<div className="flex items-center mt-2">
<a href="https://docs.usebruno.com" target="_blank" className="inline-flex items-center">
<IconBook size={18} strokeWidth={2} />
<span className="label ml-2">{dictionary.documentation}</span>
<span className="label ml-2">{t('COMMON.DOCUMENTATION')}</span>
</a>
</div>
<div className="flex items-center mt-2">
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="inline-flex items-center">
<IconSpeakerphone size={18} strokeWidth={2} />
<span className="label ml-2">{dictionary.reportIssues}</span>
<span className="label ml-2">{t('COMMON.REPORT_ISSUES')}</span>
</a>
</div>
<div className="flex items-center mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center">
<IconBrandGithub size={18} strokeWidth={2} />
<span className="label ml-2">{dictionary.gitHub}</span>
<span className="label ml-2">{t('COMMON.GITHUB')}</span>
</a>
</div>
</div>

View File

@@ -1,16 +0,0 @@
export default {
aboutBruno: 'Opensource IDE for exploring and testing APIs',
collections: 'Collections',
createCollection: 'Create Collection',
openCollection: 'Open Collection',
importCollection: 'Import Collection',
documentation: 'Documentation',
reportIssues: 'Report Issues',
gitHub: 'GitHub',
collectionImportedSuccessfully: 'Collection imported successfully',
errorWhileOpeningCollection: 'An error occurred while opening the collection',
errorWhileImportingCollection:
'An error occurred while importing the collection. Check the logs for more information.',
discord: 'Discord',
twitter: 'Twitter'
};

View File

@@ -1,5 +0,0 @@
import en from './en.js';
export const dictionaries = {
en
};

View File

@@ -0,0 +1,24 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import translationEn from './translation/en.json';
const resources = {
en: {
translation: translationEn,
},
};
i18n
.use(initReactI18next) // passes i18n down to react-i18next
.init({
resources,
lng: 'en', // Use "en" as the default language. "cimode" can be used to debug / show translation placeholder
ns: 'translation', // Use translation as the default Namespace that will be loaded by default
interpolation: {
escapeValue: false // react already safes from xss
}
});
export default i18n;

View File

@@ -0,0 +1,20 @@
{
"COMMON": {
"COLLECTIONS": "Collections",
"DOCUMENTATION": "Documentation",
"REPORT_ISSUES": "Report Issues",
"GITHUB": "GitHub",
"DISCORD": "Discord",
"TWITTER": "Twitter"
},
"WELCOME": {
"ABOUT_BRUNO": "Opensource IDE for exploring and testing APIs",
"LINKS": "Links",
"CREATE_COLLECTION": "Create Collection",
"OPEN_COLLECTION": "Open Collection",
"IMPORT_COLLECTION": "Import Collection",
"COLLECTION_IMPORT_SUCCESS": "Collection imported successfully",
"COLLECTION_IMPORT_ERROR": "An error occurred while importing the collection. Check the logs for more information.",
"COLLECTION_OPEN_ERROR": "An error occurred while opening the collection"
}
}

View File

@@ -14,7 +14,15 @@ import 'codemirror/lib/codemirror.css';
import 'graphiql/graphiql.min.css';
import 'react-tooltip/dist/react-tooltip.css';
import '@usebruno/graphql-docs/dist/esm/index.css';
import { DictionaryProvider } from 'providers/Dictionary/index';
import '@fontsource/inter/100.css';
import '@fontsource/inter/200.css';
import '@fontsource/inter/300.css';
import '@fontsource/inter/400.css';
import '@fontsource/inter/500.css';
import '@fontsource/inter/600.css';
import '@fontsource/inter/700.css';
import '@fontsource/inter/800.css';
import '@fontsource/inter/900.css';
function SafeHydrate({ children }) {
return <div suppressHydrationWarning>{typeof window === 'undefined' ? null : children}</div>;
@@ -60,15 +68,13 @@ function MyApp({ Component, pageProps }) {
<NoSsr>
<Provider store={ReduxStore}>
<ThemeProvider>
<DictionaryProvider>
<ToastProvider>
<AppProvider>
<HotkeysProvider>
<Component {...pageProps} />
</HotkeysProvider>
</AppProvider>
</ToastProvider>
</DictionaryProvider>
<ToastProvider>
<AppProvider>
<HotkeysProvider>
<Component {...pageProps} />
</HotkeysProvider>
</AppProvider>
</ToastProvider>
</ThemeProvider>
</Provider>
</NoSsr>

View File

@@ -30,12 +30,7 @@ export default class MyDocument extends Document {
render() {
return (
<Html>
<Head>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
</Head>
<Head />
<body id="bruno-app-body">
<Main />
<NextScript />

View File

@@ -1,6 +1,7 @@
import Head from 'next/head';
import Bruno from './Bruno';
import GlobalStyle from '../globalStyles';
import '../i18n';
export default function Home() {
return (

View File

@@ -60,7 +60,7 @@ const trackStart = () => {
event: 'start',
properties: {
os: platformLib.os.family,
version: '1.25.0'
version: '1.26.2'
}
});
};

View File

@@ -1,28 +0,0 @@
import React from 'react';
import { useState, useContext } from 'react';
import { dictionaries } from 'src/dictionaries/index';
export const DictionaryContext = React.createContext();
const DictionaryProvider = (props) => {
const [language, setLanguage] = useState('en');
const dictionary = dictionaries[language] ?? dictionaries.en;
return (
<DictionaryContext.Provider {...props} value={{ language, setLanguage, dictionary }}>
<>{props.children}</>
</DictionaryContext.Provider>
);
};
const useDictionary = () => {
const context = useContext(DictionaryContext);
if (context === undefined) {
throw new Error(`useDictionary must be used within a DictionaryProvider`);
}
return context;
};
export { useDictionary, DictionaryProvider };

View File

@@ -374,6 +374,7 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
});
};
// rename item
export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -718,7 +719,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
const pathParams = parsePathParams(requestUrl);
each(pathParams, (pathParm) => {
pathParams.enabled = true;
pathParm.type = 'path'
pathParm.type = 'path';
});
const params = [...queryParams, ...pathParams];

View File

@@ -41,7 +41,7 @@ export const tabsSlice = createSlice({
requestPaneTab: action.payload.requestPaneTab || 'params',
responsePaneTab: 'response',
type: action.payload.type || 'request',
...(action.payload.folderUid ? { folderUid: action.payload.folderUid } : {})
...(action.payload.uid ? { folderUid: action.payload.uid } : {})
});
state.activeTabUid = action.payload.uid;
},

View File

@@ -12,6 +12,7 @@ export const exportCollection = (collection) => {
const generateInfoSection = () => {
return {
name: collection.name,
description: collection.root?.docs,
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
};
};
@@ -137,6 +138,11 @@ export const exportCollection = (collection) => {
}
}
};
case 'graphql':
return {
mode: 'graphql',
graphql: body.graphql
};
}
};
@@ -201,6 +207,8 @@ export const exportCollection = (collection) => {
const requestObject = {
method: itemRequest.method,
header: generateHeaders(itemRequest.headers),
auth: generateAuth(itemRequest.auth),
description: itemRequest.docs,
url: {
raw: itemRequest.url,
host: generateHost(itemRequest.url),

View File

@@ -59,12 +59,15 @@ const transformOpenapiRequestItem = (request) => {
operationName = `${request.method} ${request.path}`;
}
// replace OpenAPI links in path by Bruno variables
let path = request.path.replace(/{([a-zA-Z]+)}/g, `{{${_operationObject.operationId}_$1}}`);
const brunoRequestItem = {
uid: uuid(),
name: operationName,
type: 'http-request',
request: {
url: ensureUrl(request.global.server + '/' + request.path),
url: ensureUrl(request.global.server + '/' + path),
method: request.method.toUpperCase(),
auth: {
mode: 'none',
@@ -81,6 +84,9 @@ const transformOpenapiRequestItem = (request) => {
xml: null,
formUrlEncoded: [],
multipartForm: []
},
script: {
res: null
}
}
};
@@ -195,6 +201,26 @@ const transformOpenapiRequestItem = (request) => {
}
}
// build the extraction scripts from responses that have links
// https://swagger.io/docs/specification/links/
let script = [];
each(_operationObject.responses || [], (response, responseStatus) => {
if (Object.hasOwn(response, 'links')) {
// only extract if the status code matches the response
script.push(`if (res.status === ${responseStatus}) {`);
each(response.links, (link) => {
each(link.parameters || [], (expression, parameter) => {
let value = openAPIRuntimeExpressionToScript(expression);
script.push(` bru.setVar('${link.operationId}_${parameter}', ${value});`);
});
});
script.push(`}`);
}
});
if (script.length > 0) {
brunoRequestItem.request.script.res = script.join('\n');
}
return brunoRequestItem;
};
@@ -305,6 +331,18 @@ const getSecurity = (apiSpec) => {
};
};
const openAPIRuntimeExpressionToScript = (expression) => {
// see https://swagger.io/docs/specification/links/#runtime-expressions
if (expression === '$response.body') {
return 'res.body';
} else if (expression.startsWith('$response.body#')) {
let pointer = expression.substring(15);
// could use https://www.npmjs.com/package/json-pointer for better support
return `res.body${pointer.replace('/', '.')}`;
}
return expression;
};
const parseOpenApiCollection = (data) => {
const brunoCollection = {
name: '',
@@ -348,7 +386,7 @@ const parseOpenApiCollection = (data) => {
.map(([method, operationObject]) => {
return {
method: method,
path: path,
path: path.replace(/{([^}]+)}/g, ':$1'), // Replace placeholders enclosed in curly braces with colons
operationObject: operationObject,
global: {
server: baseUrl,

View File

@@ -113,7 +113,8 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
xml: null,
formUrlEncoded: [],
multipartForm: []
}
},
docs: i.request.description
}
};
/* struct of translation log

View File

@@ -44,7 +44,8 @@ export const parsePathParams = (url) => {
try {
uri = new URL(uri);
} catch (e) {
throw e;
// URL is non-parsable, is it incomplete? Ignore.
return [];
}
let paths = uri.pathname.split('/');

View File

@@ -14,7 +14,7 @@
"url": "git+https://github.com/usebruno/bruno.git"
},
"scripts": {
"test": "node --experimental-vm-modules $(npx --no-install which jest)"
"test": "node --experimental-vm-modules $(npx which jest)"
},
"files": [
"src",

View File

@@ -241,7 +241,7 @@ const builder = async (yargs) => {
})
.option('tests-only', {
type: 'boolean',
description: 'Only run requests that have a test'
description: 'Only run requests that have a test or active assertion'
})
.option('bail', {
type: 'boolean',

View File

@@ -22,13 +22,13 @@
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
"rollup": "3.2.5",
"rollup":"3.29.4",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.8.4"
},
"overrides": {
"rollup": "3.2.5"
"rollup":"3.29.4"
}
}

View File

@@ -294,7 +294,7 @@ describe('interpolate - recursive', () => {
const result = interpolate(inputString, inputObject);
expect(result).toBe('{{recursion2}}');
expect(result).toBe('{{recursion3}}');
});
it('should replace repetead placeholders with 1 level of recursion with values from the object', () => {
@@ -335,4 +335,22 @@ describe('interpolate - recursive', () => {
expect(result).toBe(new Array(24).fill('repetead4').join(' '));
});
it('should replace mutiple interdependent variables in the same input string', () => {
const inputString = `{
"x": "{{v2}} {{v1}}"
}`;
const inputObject = {
foo: 'bar',
v1: '{{foo}}',
v2: '{{bar}}',
bar: 'baz'
};
const result = interpolate(inputString, inputObject);
expect(result).toBe(`{
"x": "baz bar"
}`);
});
});

View File

@@ -27,32 +27,41 @@ const interpolate = (str: string, obj: Record<string, any>): string => {
const replace = (
str: string,
flattenedObj: Record<string, any>,
visited = new Set<String>(),
visited = new Set<string>(),
results = new Map<string, string>()
): string => {
const patternRegex = /\{\{([^}]+)\}\}/g;
let resultStr = str;
let matchFound = true;
return str.replace(patternRegex, (match, placeholder) => {
const replacement = flattenedObj[placeholder];
while (matchFound) {
const patternRegex = /\{\{([^}]+)\}\}/g;
matchFound = false;
resultStr = resultStr.replace(patternRegex, (match, placeholder) => {
const replacement = flattenedObj[placeholder];
if (results.has(match)) {
return results.get(match);
}
if (results.has(match)) {
return results.get(match);
}
if (patternRegex.test(replacement) && !visited.has(match)) {
visited.add(match);
const result = replace(replacement, flattenedObj, visited, results);
results.set(match, result);
matchFound = true;
return result;
}
if (patternRegex.test(replacement) && !visited.has(match)) {
visited.add(match);
const result = replace(replacement, flattenedObj, visited, results);
const result = replacement !== undefined ? replacement : match;
results.set(match, result);
matchFound = true;
return result;
}
});
}
visited.add(match);
const result = replacement !== undefined ? replacement : match;
results.set(match, result);
return result;
});
return resultStr;
};
export default interpolate;

View File

@@ -1,5 +1,5 @@
{
"version": "v1.26.0",
"version": "v1.26.2",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",
@@ -16,7 +16,7 @@
"dist:rpm": "electron-builder --linux rpm --config electron-builder-config.js",
"dist:snap": "electron-builder --linux snap --config electron-builder-config.js",
"pack": "electron-builder --dir",
"test": "node --experimental-vm-modules $(npx --no-install which jest)"
"test": "node --experimental-vm-modules $(npx which jest)"
},
"jest": {
"modulePaths": ["node_modules"]

View File

@@ -1,5 +1,15 @@
const fs = require('fs');
const path = require('path');
const isDev = require('electron-is-dev');
if (isDev) {
if (!fs.existsSync('./src/sandbox/bundle-browser-rollup.js')) {
console.log('JS Sandbox libraries have not been bundled yet');
console.log('Please run the below command \nnpm run sandbox:bundle-libraries --workspace=packages/bruno-js');
throw new Error('JS Sandbox libraries have not been bundled yet');
}
}
const { format } = require('url');
const { BrowserWindow, app, Menu, ipcMain } = require('electron');
const { setContentSecurityPolicy } = require('electron-util');
@@ -70,7 +80,7 @@ app.on('ready', async () => {
mainWindow.once('ready-to-show', () => {
mainWindow.show();
})
});
const url = isDev
? 'http://localhost:3000'
: format({

View File

@@ -13,7 +13,9 @@ const {
browseFiles,
createDirectory,
searchForBruFiles,
sanitizeDirectoryName
sanitizeDirectoryName,
isWSLPath,
normalizeWslPath,
} = require('../utils/filesystem');
const { openCollectionDialog } = require('../app/collections');
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
@@ -326,6 +328,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// rename item
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
try {
// Normalize paths if they are WSL paths
if (isWSLPath(oldPath)) {
oldPath = normalizeWslPath(oldPath);
}
if (isWSLPath(newPath)) {
newPath = normalizeWslPath(newPath);
}
if (!fs.existsSync(oldPath)) {
throw new Error(`path: ${oldPath} does not exist`);
}

View File

@@ -29,7 +29,7 @@ const { makeAxiosInstance } = require('./axios-instance');
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
const { addDigestInterceptor } = require('./digestauth-helper');
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util');
const { chooseFileToSave, writeBinaryFile } = require('../../utils/filesystem');
const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem');
const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies');
const {
resolveOAuth2AuthorizationCodeAccessToken,
@@ -272,7 +272,7 @@ const configureRequest = async (
return axiosInstance;
};
const parseDataFromResponse = (response) => {
const parseDataFromResponse = (response, disableParsingResponseJson = false) => {
// Parse the charset from content type: https://stackoverflow.com/a/33192813
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || '');
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals
@@ -290,7 +290,9 @@ const parseDataFromResponse = (response) => {
// Filter out ZWNBSP character
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
data = data.replace(/^\uFEFF/, '');
data = JSON.parse(data);
if(!disableParsingResponseJson) {
data = JSON.parse(data);
}
} catch {}
return { data, dataBuffer };
@@ -540,7 +542,7 @@ const registerNetworkIpc = (mainWindow) => {
// Continue with the rest of the request lifecycle - post response vars, script, assertions, tests
const { data, dataBuffer } = parseDataFromResponse(response);
const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
response.data = data;
response.responseTime = responseTime;
@@ -701,7 +703,7 @@ const registerNetworkIpc = (mainWindow) => {
}
}
const { data } = parseDataFromResponse(response);
const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
response.data = data;
await runPostResponse(
@@ -969,7 +971,7 @@ const registerNetworkIpc = (mainWindow) => {
response = await axiosInstance(request);
timeEnd = Date.now();
const { data, dataBuffer } = parseDataFromResponse(response);
const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
response.data = data;
response.responseTime = response.headers.get('request-duration');
@@ -1189,7 +1191,13 @@ const registerNetworkIpc = (mainWindow) => {
const fileName = determineFileName();
const filePath = await chooseFileToSave(mainWindow, fileName);
if (filePath) {
await writeBinaryFile(filePath, Buffer.from(response.dataBuffer, getEncodingFormat()));
const encoding = getEncodingFormat();
const data = Buffer.from(response.dataBuffer, 'base64')
if (encoding === 'utf-8') {
await writeFile(filePath, data);
} else {
await writeBinaryFile(filePath, data);
}
}
} catch (error) {
return Promise.reject(error);

View File

@@ -59,14 +59,6 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
const contentType = getContentType(request.headers);
if (contentType.includes('json')) {
if (typeof request.data === 'object') {
try {
let parsed = JSON.stringify(request.data);
parsed = _interpolate(parsed);
request.data = JSON.parse(parsed);
} catch (err) {}
}
if (typeof request.data === 'string') {
if (request.data.length) {
request.data = _interpolate(request.data);

View File

@@ -1,7 +1,6 @@
const os = require('os');
const { get, each, filter, extend, compact } = require('lodash');
const decomment = require('decomment');
var JSONbig = require('json-bigint');
const FormData = require('form-data');
const fs = require('fs');
const path = require('path');
@@ -342,16 +341,10 @@ const prepareRequest = (item, collection) => {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/json';
}
let jsonBody;
try {
jsonBody = decomment(request?.body?.json);
axiosRequest.data = decomment(request?.body?.json);
} catch (error) {
jsonBody = request?.body?.json;
}
try {
axiosRequest.data = JSONbig.parse(jsonBody);
} catch (error) {
axiosRequest.data = jsonBody;
axiosRequest.data = request?.body?.json;
}
}

View File

@@ -50,6 +50,18 @@ const normalizeAndResolvePath = (pathname) => {
return path.resolve(pathname);
};
function isWSLPath(pathname) {
// Check if the path starts with the WSL prefix
// eg. "\\wsl.localhost\Ubuntu\home\user\bruno\collection\scripting\api\req\getHeaders.bru"
return pathname.startsWith('/wsl.localhost/') || pathname.startsWith('\\wsl.localhost\\');
}
function normalizeWslPath(pathname) {
// Replace the WSL path prefix and convert forward slashes to backslashes
// This is done to achieve WSL paths (linux style) to Windows UNC equivalent (Universal Naming Conversion)
return pathname.replace(/^\/wsl.localhost/, '\\\\wsl.localhost').replace(/\//g, '\\');
}
const writeFile = async (pathname, content) => {
try {
fs.writeFileSync(pathname, content, {
@@ -143,6 +155,8 @@ const searchForBruFiles = (dir) => {
return searchForFiles(dir, '.bru');
};
// const isW
const sanitizeDirectoryName = (name) => {
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-');
};
@@ -154,6 +168,8 @@ module.exports = {
isFile,
isDirectory,
normalizeAndResolvePath,
isWSLPath,
normalizeWslPath,
writeFile,
writeBinaryFile,
hasJsonExtension,

View File

@@ -6,7 +6,7 @@ describe('prepare-request: prepareRequest', () => {
describe('Decomments request body', () => {
it('If request body is valid JSON', async () => {
const body = { mode: 'json', json: '{\n"test": "{{someVar}}" // comment\n}' };
const expected = { test: '{{someVar}}' };
const expected = '{\n"test": "{{someVar}}" \n}';
const result = prepareRequest({ request: { body } }, {});
expect(result.data).toEqual(expected);
});

View File

@@ -22,7 +22,7 @@
"postcss": "^8.4.18",
"react": "18.2.0",
"react-dom": "18.2.0",
"rollup": "3.2.5",
"rollup":"3.29.4",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.2",
@@ -34,6 +34,6 @@
"markdown-it": "^13.0.1"
},
"overrides": {
"rollup": "3.2.5"
"rollup":"3.29.4"
}
}

View File

@@ -11,7 +11,7 @@
"@n8n/vm2": "^3.9.23"
},
"scripts": {
"test": "node --experimental-vm-modules $(npx --no-install which jest) --testPathIgnorePatterns test.js",
"test": "node --experimental-vm-modules $(npx which jest) --testPathIgnorePatterns test.js",
"sandbox:bundle-libraries": "node ./src/sandbox/bundle-libraries.js"
},
"dependencies": {

View File

@@ -1,11 +1,34 @@
class BrunoRequest {
/**
* The following properties are available as shorthand:
* - req.url
* - req.method
* - req.headers
* - req.timeout
* - req.body
*
* Above shorthands are useful for accessing the request properties directly in the scripts
* It must be noted that the user cannot set these properties directly.
* They should use the respective setter methods to set these properties.
*/
constructor(req) {
this.req = req;
this.url = req.url;
this.method = req.method;
this.headers = req.headers;
this.body = req.data;
this.timeout = req.timeout;
/**
* We automatically parse the JSON body if the content type is JSON
* This is to make it easier for the user to access the body directly
*
* It must be noted that the request data is always a string and is what gets sent over the network
* If the user wants to access the raw data, they can use getBody({raw: true}) method
*/
const isJson = this.hasJSONContentType(this.req.headers);
if (isJson) {
this.body = this.__safeParseJSON(req.data);
}
}
getUrl() {
@@ -13,6 +36,7 @@ class BrunoRequest {
}
setUrl(url) {
this.url = url;
this.req.url = url;
}
@@ -37,6 +61,7 @@ class BrunoRequest {
}
setMethod(method) {
this.method = method;
this.req.method = method;
}
@@ -45,6 +70,7 @@ class BrunoRequest {
}
setHeaders(headers) {
this.headers = headers;
this.req.headers = headers;
}
@@ -53,15 +79,60 @@ class BrunoRequest {
}
setHeader(name, value) {
this.headers[name] = value;
this.req.headers[name] = value;
}
getBody() {
hasJSONContentType(headers) {
const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
return contentType.includes('json');
}
/**
* Get the body of the request
*
* We automatically parse and return the JSON body if the content type is JSON
* If the user wants the raw body, they can pass the raw option as true
*/
getBody(options = {}) {
if (options.raw) {
return this.req.data;
}
const isJson = this.hasJSONContentType(this.req.headers);
if (isJson) {
return this.__safeParseJSON(this.req.data);
}
return this.req.data;
}
setBody(data) {
/**
* If the content type is JSON and if the data is an object
* - We set the body property as the object itself
* - We set the request data as the stringified JSON as it is what gets sent over the network
* Otherwise
* - We set the request data as the data itself
* - We set the body property as the data itself
*
* If the user wants to override this behavior, they can pass the raw option as true
*/
setBody(data, options = {}) {
if (options.raw) {
this.req.data = data;
this.body = data;
return;
}
const isJson = this.hasJSONContentType(this.req.headers);
if (isJson && this.__isObject(data)) {
this.body = data;
this.req.data = this.__safeStringifyJSON(data);
return;
}
this.req.data = data;
this.body = data;
}
setMaxRedirects(maxRedirects) {
@@ -73,8 +144,34 @@ class BrunoRequest {
}
setTimeout(timeout) {
this.timeout = timeout;
this.req.timeout = timeout;
}
__safeParseJSON(str) {
try {
return JSON.parse(str);
} catch (e) {
return str;
}
}
__safeStringifyJSON(obj) {
try {
return JSON.stringify(obj);
} catch (e) {
return obj;
}
}
__isObject(obj) {
return obj !== null && typeof obj === 'object';
}
disableParsingResponseJson() {
this.req.__brunoDisableParsingResponseJson = true;
}
}
module.exports = BrunoRequest;

View File

@@ -15,7 +15,7 @@ const BrunoRequest = require('../bruno-request');
const BrunoResponse = require('../bruno-response');
const Test = require('../test');
const TestResults = require('../test-results');
const { cleanJson, appendAwaitToTestFunc } = require('../utils');
const { cleanJson } = require('../utils');
// Inbuilt Library Support
const ajv = require('ajv');
@@ -84,8 +84,6 @@ class TestRuntime {
};
}
// add 'await' prefix to the test function calls
testsFile = appendAwaitToTestFunc(testsFile);
const context = {
test,

View File

@@ -10,6 +10,7 @@ const { newQuickJSWASMModule, memoizePromiseFactory } = require('quickjs-emscrip
// execute `npm run sandbox:bundle-libraries` if the below file doesn't exist
const getBundledCode = require('../bundle-browser-rollup');
const addPathShimToContext = require('./shims/lib/path');
const { marshallToVm } = require('./utils');
let QuickJSSyncContext;
const loader = memoizePromiseFactory(() => newQuickJSWASMModule());
@@ -21,22 +22,45 @@ const toNumber = (value) => {
return Number.isInteger(num) ? parseInt(value, 10) : parseFloat(value);
};
const removeQuotes = (str) => {
if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
return str.slice(1, -1);
}
return str;
};
const executeQuickJsVm = ({ script: externalScript, context: externalContext, scriptType = 'template-literal' }) => {
if (!externalScript?.length || typeof externalScript !== 'string') {
return externalScript;
}
externalScript = externalScript?.trim();
if (!isNaN(Number(externalScript))) {
return Number(externalScript);
}
if (externalScript === 'true') return true;
if (externalScript === 'false') return false;
if (externalScript === 'null') return null;
if (externalScript === 'undefined') return undefined;
externalScript = removeQuotes(externalScript);
const vm = QuickJSSyncContext;
try {
const { bru, req, res } = externalContext;
const { bru, req, res, ...variables } = externalContext;
bru && addBruShimToContext(vm, bru);
req && addBrunoRequestShimToContext(vm, req);
res && addBrunoResponseShimToContext(vm, res);
const templateLiteralText = `\`${externalScript}\`;`;
const jsExpressionText = `${externalScript};`;
Object.entries(variables)?.forEach(([key, value]) => {
vm.setProp(vm.global, key, marshallToVm(value, vm));
});
const templateLiteralText = `\`${externalScript}\``;
const jsExpressionText = `${externalScript}`;
let scriptText = scriptType === 'template-literal' ? templateLiteralText : jsExpressionText;
@@ -47,7 +71,6 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
return e;
} else {
let v = vm.dump(result.value);
let vString = v.toString();
result.value.dispose();
return v;
}
@@ -57,9 +80,22 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
};
const executeQuickJsVmAsync = async ({ script: externalScript, context: externalContext, collectionPath }) => {
if (!externalScript?.length || typeof externalScript !== 'string') {
return externalScript;
}
externalScript = externalScript?.trim();
if (!isNaN(Number(externalScript))) {
return toNumber(externalScript);
}
if (externalScript === 'true') return true;
if (externalScript === 'false') return false;
if (externalScript === 'null') return null;
if (externalScript === 'undefined') return undefined;
externalScript = removeQuotes(externalScript);
try {
const module = await newQuickJSWASMModule();
const vm = module.newContext();
@@ -92,6 +128,9 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
// resolve module
return globalThis.requireObject[mod];
}
else {
throw new Error("Cannot find module " + mod);
}
}
`;
};
@@ -128,6 +167,7 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
}
catch(error) {
console?.debug?.('quick-js:execution-end:with-error', error?.message);
throw new Error(error?.message);
}
return 'done';
})()

View File

@@ -63,6 +63,12 @@ const addBruShimToContext = (vm, bru) => {
vm.setProp(bruObject, 'getSecretVar', getSecretVar);
getSecretVar.dispose();
let getRequestVar = vm.newFunction('getRequestVar', function (key) {
return marshallToVm(bru.getRequestVar(vm.dump(key)), vm);
});
vm.setProp(bruObject, 'getRequestVar', getRequestVar);
getRequestVar.dispose();
const sleep = vm.newFunction('sleep', (timer) => {
const t = vm.getString(timer);
const promise = vm.newPromise();

View File

@@ -19,6 +19,10 @@ const addLocalModuleLoaderShimToContext = (vm, collectionPath) => {
throw new Error('Access to files outside of the collectionPath is not allowed.');
}
if (!fs.existsSync(filePath)) {
throw new Error(`Cannot find module ${filename}`);
}
let code = fs.readFileSync(filePath).toString();
return marshallToVm(code, vm);

View File

@@ -10,7 +10,7 @@ const marshallToVm = (value, vm) => {
} else if (typeof value === 'number') {
return vm.newNumber(value);
} else if (typeof value === 'boolean') {
return vm.newBoolean(value);
return value ? vm.true : vm.false;
} else if (typeof value === 'object') {
if (Array.isArray(value)) {
const arr = vm.newArray();

View File

@@ -142,15 +142,10 @@ const cleanJson = (data) => {
}
};
const appendAwaitToTestFunc = (str) => {
return str.replace(/(?<!\.\s*)(?<!await\s)(test\()/g, 'await $1');
};
module.exports = {
evaluateJsExpression,
evaluateJsTemplateLiteral,
createResponseParser,
internalExpressionCache,
cleanJson,
appendAwaitToTestFunc
cleanJson
};

View File

@@ -1,10 +1,5 @@
const { describe, it, expect } = require('@jest/globals');
const {
evaluateJsExpression,
internalExpressionCache: cache,
createResponseParser,
appendAwaitToTestFunc
} = require('../src/utils');
const { evaluateJsExpression, internalExpressionCache: cache, createResponseParser } = require('../src/utils');
describe('utils', () => {
describe('expression evaluation', () => {
@@ -142,70 +137,4 @@ describe('utils', () => {
expect(value).toBe(20);
});
});
describe('appendAwaitToTestFunc function', () => {
it('example 1', () => {
const inputTestsString = `
test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
`;
const ouutputTestsString = appendAwaitToTestFunc(inputTestsString);
expect(ouutputTestsString).toBe(`
await test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
`);
});
it('example 2', () => {
const inputTestsString = `
await test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
`;
const ouutputTestsString = appendAwaitToTestFunc(inputTestsString);
expect(ouutputTestsString).toBe(`
await test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
await test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
await test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
`);
});
});
});

View File

@@ -21,13 +21,13 @@
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
"rollup": "3.2.5",
"rollup":"3.29.4",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.8.4"
},
"overrides": {
"rollup": "3.2.5"
"rollup":"3.29.4"
}
}

View File

@@ -0,0 +1,44 @@
meta {
name: echo bigint
type: http
seq: 6
}
post {
url: {{host}}/api/echo/json
body: json
auth: none
}
headers {
foo: bar
}
auth:basic {
username: asd
password: j
}
auth:bearer {
token:
}
body:json {
{
"hello": 990531470713421825
}
}
body:text {
{
"hello": 990531470713421825
}
}
assert {
res.status: eq 200
}
script:pre-request {
bru.setVar("foo", "foo-world-2");
}

Some files were not shown because too many files have changed in this diff Show More