mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 09:28:33 +00:00
Compare commits
48 Commits
v1.26.0
...
fix/json-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00e98451d4 | ||
|
|
b4fd350334 | ||
|
|
ad388e5a6a | ||
|
|
4ddccefee3 | ||
|
|
b121afe7bb | ||
|
|
f6c6a3b2bf | ||
|
|
cf02ea2572 | ||
|
|
9343f1e070 | ||
|
|
d000625c39 | ||
|
|
4bdbfb5c0c | ||
|
|
4fbd2f0bdb | ||
|
|
5ae3f0e75a | ||
|
|
b80269b68f | ||
|
|
67de396927 | ||
|
|
65b80cfd06 | ||
|
|
caa0a22e74 | ||
|
|
c7f0335d96 | ||
|
|
22fab7f599 | ||
|
|
c0d214f2bc | ||
|
|
9efbd7377a | ||
|
|
b63cf46734 | ||
|
|
73ac969d35 | ||
|
|
dc21206fc0 | ||
|
|
8f58235e17 | ||
|
|
fd6b3630a5 | ||
|
|
25e57d2578 | ||
|
|
cef6f85845 | ||
|
|
5f0e6f13eb | ||
|
|
0b9554c8cc | ||
|
|
8b76ecede3 | ||
|
|
44d70ca02a | ||
|
|
4d55b50250 | ||
|
|
6320a80cbe | ||
|
|
d1c34bd379 | ||
|
|
71ffe1f8d4 | ||
|
|
04ccb2f6ee | ||
|
|
99ddd8021c | ||
|
|
ee8e162f3d | ||
|
|
b1a140a4e0 | ||
|
|
e4407f3981 | ||
|
|
74e75a7da2 | ||
|
|
4aff61b665 | ||
|
|
df2e18bedd | ||
|
|
9a88db7e56 | ||
|
|
4a4439f48e | ||
|
|
4710928407 | ||
|
|
33804f4c7b | ||
|
|
e0858d1c99 |
@@ -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
|
||||
|
||||
|
||||
@@ -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_)。
|
||||
|
||||
## 开发
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 वर्कस्पेस का उपयोग करते हैं
|
||||
|
||||
## डेवलपमेंट
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 ワークスペースを使用しています。
|
||||
|
||||
## 開発
|
||||
|
||||
|
||||
@@ -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를 사용합니다.
|
||||
|
||||
## 개발
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
### Приступим к коду
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 в цьому проекті
|
||||
|
||||
### Починаєм писати код
|
||||
|
||||
|
||||
@@ -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_)。
|
||||
|
||||
## 開發
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
| [正體中文](./readme_zhtw.md)
|
||||
| **العربية**
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
|
||||
برونو هو عميل API جديد ومبتكر، يهدف إلى ثورة الحالة الحالية التي يمثلها برنامج Postman وأدوات مماثلة هناك.
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
| [正體中文](./readme_zhtw.md)
|
||||
| [العربية](./readme_ar.md)
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
|
||||
ব্রুনো হল একটি নতুন এবং উদ্ভাবনী API ক্লায়েন্ট, যার লক্ষ্য পোস্টম্যান এবং অনুরূপ সরঞ্জাম দ্বারা প্রতিনিধিত্ব করা স্থিতাবস্থায় বিপ্লব ঘটানো।
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
| [正體中文](./readme_zhtw.md)
|
||||
| [العربية](./readme_ar.md)
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
|
||||
Bruno 是一款全新且创新的 API 客户端,旨在颠覆 Postman 和其他类似工具。
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
176
docs/readme/readme_ka.md
Normal file
@@ -0,0 +1,176 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### ბრუნო - ღია წყაროების IDE API-ების შესწავლისა და ტესტირებისათვის.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](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)
|
||||
|
||||
 <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
|
||||
```
|
||||
|
||||
### პლატფორმებს შორის მუშაობა 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### თანამშრომლობა Git-ის საშუალებით 👩💻🧑💻
|
||||
|
||||
ან ნებისმიერი ვერსიის კონტროლის სისტემის საშუალებით
|
||||
|
||||
 <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)
|
||||
@@ -27,6 +27,7 @@
|
||||
| [正體中文](./readme_zhtw.md)
|
||||
| [العربية](./readme_ar.md)
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
|
||||
Bruno는 새롭고 혁신적인 API 클라이언트로, Postman과 유사한 툴들을 혁신하는 것을 목표로 합니다.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
| [正體中文](./readme_zhtw.md)
|
||||
| [العربية](./readme_ar.md)
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
|
||||
Bruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
| [正體中文](./readme_zhtw.md)
|
||||
| [العربية](./readme_ar.md)
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
|
||||
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman.
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
| **正體中文**
|
||||
| [العربية](./readme_ar.md)
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
|
||||
Bruno 是一個全新且有創新性的 API 用戶端,目的在徹底改變以 Postman 和其他類似工具的現況。
|
||||
|
||||
|
||||
6354
package-lock.json
generated
6354
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
})
|
||||
);
|
||||
|
||||
16
packages/bruno-app/src/components/Icons/Dot/index.js
Normal file
16
packages/bruno-app/src/components/Icons/Dot/index.js
Normal 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;
|
||||
@@ -69,6 +69,7 @@ const StyledMarkdownBodyWrapper = styled.div`
|
||||
|
||||
pre {
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
table {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
+ <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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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)));
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import en from './en.js';
|
||||
|
||||
export const dictionaries = {
|
||||
en
|
||||
};
|
||||
24
packages/bruno-app/src/i18n/index.js
Normal file
24
packages/bruno-app/src/i18n/index.js
Normal 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;
|
||||
20
packages/bruno-app/src/i18n/translation/en.json
Normal file
20
packages/bruno-app/src/i18n/translation/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Head from 'next/head';
|
||||
import Bruno from './Bruno';
|
||||
import GlobalStyle from '../globalStyles';
|
||||
import '../i18n';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
|
||||
@@ -60,7 +60,7 @@ const trackStart = () => {
|
||||
event: 'start',
|
||||
properties: {
|
||||
os: platformLib.os.family,
|
||||
version: '1.25.0'
|
||||
version: '1.26.2'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -113,7 +113,8 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
|
||||
xml: null,
|
||||
formUrlEncoded: [],
|
||||
multipartForm: []
|
||||
}
|
||||
},
|
||||
docs: i.request.description
|
||||
}
|
||||
};
|
||||
/* struct of translation log
|
||||
|
||||
@@ -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('/');
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
})()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
});
|
||||
});
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
44
packages/bruno-tests/collection/echo/echo bigint.bru
Normal file
44
packages/bruno-tests/collection/echo/echo bigint.bru
Normal 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
Reference in New Issue
Block a user