Compare commits

..

6 Commits

Author SHA1 Message Date
Anoop M D
758ef9fc96 feat: async parser workers (#3834) 2025-01-29 02:53:10 +05:30
Anoop M D
ced9d38abc feat: async parser workers (#3834) 2025-01-29 02:44:32 +05:30
lohit
98f3a524dc feat: updated the bru async parsing logic (#3898) 2025-01-28 21:26:46 +05:30
Anoop M D
a06a339d0c feat: async parser workers (#3834) 2025-01-27 23:44:17 +05:30
Anoop M D
e34ac3de7c feat: async parser workers (#3834) 2025-01-27 15:33:05 +05:30
lohit
074c6be5f4 feat: async parser workers (#3834)
* feat: async parser workers (#3834)
---------
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2025-01-27 12:26:32 +05:30
90 changed files with 344 additions and 2115 deletions

View File

@@ -5,13 +5,14 @@ on:
pull_request:
branches: [main]
permissions:
contents: read
jobs:
unit-test:
name: Unit Tests
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@@ -51,10 +52,6 @@ jobs:
cli-test:
name: CLI Tests
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

448
package-lock.json generated
View File

@@ -8586,6 +8586,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"dev": true,
"license": "ISC"
},
"node_modules/boolean": {
@@ -9416,229 +9417,6 @@
"node": "*"
}
},
"node_modules/cheerio": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
"integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==",
"license": "MIT",
"dependencies": {
"cheerio-select": "^2.1.0",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"encoding-sniffer": "^0.2.0",
"htmlparser2": "^9.1.0",
"parse5": "^7.1.2",
"parse5-htmlparser2-tree-adapter": "^7.0.0",
"parse5-parser-stream": "^7.1.2",
"undici": "^6.19.5",
"whatwg-mimetype": "^4.0.0"
},
"engines": {
"node": ">=18.17"
},
"funding": {
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
}
},
"node_modules/cheerio-select": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-select": "^5.1.0",
"css-what": "^6.1.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/cheerio-select/node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/cheerio-select/node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/cheerio-select/node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/cheerio-select/node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/cheerio-select/node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/cheerio-select/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/cheerio/node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/cheerio/node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/cheerio/node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/cheerio/node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/cheerio/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/cheerio/node_modules/htmlparser2": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"entities": "^4.5.0"
}
},
"node_modules/cheerio/node_modules/parse5": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
"license": "MIT",
"dependencies": {
"entities": "^4.5.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -10805,6 +10583,7 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
@@ -11946,19 +11725,6 @@
"iconv-lite": "^0.6.2"
}
},
"node_modules/encoding-sniffer": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
"integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==",
"license": "MIT",
"dependencies": {
"iconv-lite": "^0.6.3",
"whatwg-encoding": "^3.1.1"
},
"funding": {
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -17611,6 +17377,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
@@ -18015,106 +17782,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/parse5-htmlparser2-tree-adapter": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
"license": "MIT",
"dependencies": {
"domhandler": "^5.0.3",
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-htmlparser2-tree-adapter/node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/parse5-htmlparser2-tree-adapter/node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/parse5-htmlparser2-tree-adapter/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
"license": "MIT",
"dependencies": {
"entities": "^4.5.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-parser-stream": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
"license": "MIT",
"dependencies": {
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-parser-stream/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/parse5-parser-stream/node_modules/parse5": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
"license": "MIT",
"dependencies": {
"entities": "^4.5.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -21503,6 +21170,7 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
"dev": true,
"license": "ISC"
},
"node_modules/scheduler": {
@@ -23561,15 +23229,6 @@
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
"license": "MIT"
},
"node_modules/undici": {
"version": "6.21.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
@@ -24098,27 +23757,6 @@
"node": ">=10.13.0"
}
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
@@ -24260,28 +23898,6 @@
"node": ">= 16"
}
},
"node_modules/xml2js": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
"license": "MIT",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/xml2js/node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/xmlbuilder": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
@@ -24414,6 +24030,7 @@
"@usebruno/common": "0.1.0",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.7.0",
"axios": "1.7.5",
"classnames": "^2.3.1",
"codemirror": "5.65.2",
"codemirror-graphql": "2.1.1",
@@ -24427,7 +24044,7 @@
"graphiql": "3.7.1",
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
"httpsnippet": "^3.0.9",
"httpsnippet": "^3.0.6",
"i18next": "24.1.2",
"idb": "^7.0.0",
"immer": "^9.0.15",
@@ -24530,7 +24147,7 @@
"@usebruno/lang": "0.12.0",
"@usebruno/vm2": "^3.9.13",
"aws4-axios": "^3.3.0",
"axios": "1.7.7",
"axios": "1.7.5",
"axios-ntlm": "^1.4.2",
"chai": "^4.3.7",
"chalk": "^3.0.0",
@@ -24551,16 +24168,6 @@
"bru": "bin/bru.js"
}
},
"packages/bruno-cli/node_modules/axios": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"packages/bruno-cli/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -24603,7 +24210,7 @@
"@usebruno/vm2": "^3.9.13",
"about-window": "^1.15.2",
"aws4-axios": "^3.3.0",
"axios": "1.7.7",
"axios": "1.7.5",
"axios-ntlm": "^1.4.2",
"chai": "^4.3.7",
"chokidar": "^3.5.3",
@@ -24640,17 +24247,6 @@
"dmg-license": "^1.0.11"
}
},
"packages/bruno-electron/node_modules/axios": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"packages/bruno-electron/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -24745,11 +24341,10 @@
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"atob": "^2.1.2",
"axios": "1.7.7",
"axios": "1.7.5",
"btoa": "^1.2.1",
"chai": "^4.3.7",
"chai-string": "^1.5.0",
"cheerio": "^1.0.0",
"crypto-js": "^4.1.1",
"json-query": "^2.2.2",
"lodash": "^4.17.21",
@@ -24759,8 +24354,7 @@
"node-vault": "^0.10.2",
"path": "^0.12.7",
"quickjs-emscripten": "^0.29.2",
"uuid": "^9.0.0",
"xml2js": "^0.6.2"
"uuid": "^9.0.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.2",
@@ -24774,16 +24368,6 @@
"@usebruno/vm2": "^3.9.13"
}
},
"packages/bruno-js/node_modules/axios": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"packages/bruno-js/node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
@@ -24861,7 +24445,7 @@
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"axios": "1.7.7",
"axios": "1.7.5",
"body-parser": "1.20.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
@@ -24875,16 +24459,6 @@
"multer": "^1.4.5-lts.1"
}
},
"packages/bruno-tests/node_modules/axios": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"packages/bruno-tests/node_modules/fast-xml-parser": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.1.tgz",

View File

@@ -36,7 +36,6 @@
"build:web": "npm run build --workspace=packages/bruno-app",
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",

View File

@@ -20,6 +20,7 @@
"@usebruno/common": "0.1.0",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.7.0",
"axios": "1.7.5",
"classnames": "^2.3.1",
"codemirror": "5.65.2",
"codemirror-graphql": "2.1.1",
@@ -33,7 +34,7 @@
"graphiql": "3.7.1",
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
"httpsnippet": "^3.0.9",
"httpsnippet": "^3.0.6",
"i18next": "24.1.2",
"idb": "^7.0.0",
"immer": "^9.0.15",

View File

@@ -8,8 +8,6 @@ const StyledWrapper = styled.div`
font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};
line-break: anywhere;
flex: 1 1 0;
display: flex;
flex-direction: column-reverse;
}
/* Removes the glow outline around the folded json */
@@ -28,10 +26,6 @@ const StyledWrapper = styled.div`
.CodeMirror-dialog {
overflow: visible;
position: relative;
top: unset;
left: unset;
input {
background: transparent;
border: 1px solid #d3d6db;

View File

@@ -74,16 +74,13 @@ if (!SERVER_RENDERED) {
'bru.setNextRequest(requestName)',
'req.disableParsingResponseJson()',
'bru.getRequestVar(key)',
'bru.runRequest(requestPathName)',
'bru.getAssertionResults()',
'bru.getTestResults()',
'bru.sleep(ms)',
'bru.getGlobalEnvVar(key)',
'bru.setGlobalEnvVar(key, value)',
'bru.runner',
'bru.runner.setNextRequest(requestName)',
'bru.runner.skipRequest()',
'bru.runner.stopExecution()',
'bru.runner.stopExecution()'
];
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
const cursor = editor.getCursor();
@@ -197,20 +194,8 @@ export default class CodeEditor extends React.Component {
'Cmd-Y': 'foldAll',
'Ctrl-I': 'unfoldAll',
'Cmd-I': 'unfoldAll',
'Ctrl-/': () => {
if (['application/ld+json', 'application/json'].includes(this.props.mode)) {
this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });
} else {
this.editor.toggleComment();
}
},
'Cmd-/': () => {
if (['application/ld+json', 'application/json'].includes(this.props.mode)) {
this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });
} else {
this.editor.toggleComment();
}
}
'Ctrl-/': 'toggleComment',
'Cmd-/': 'toggleComment'
},
foldOptions: {
widget: (from, to) => {

View File

@@ -104,15 +104,18 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<div className="mb-3 flex items-center">
<label className="settings-label flex items-center" htmlFor="enabled">
Config
<InfoTip infotipId="request-var">
<InfoTip
text={`
<div>
<ul>
<li><span style={{width: "50px", display: "inline-block"}}>global</span> - use global proxy config</li>
<li><span style={{width: "50px", display: "inline-block"}}>enabled</span> - use collection proxy config</li>
<li><span style={{width: "50px", display: "inline-block"}}>disable</span> - disable proxy</li>
<li><span style="width: 50px;display:inline-block;">global</span> - use global proxy config</li>
<li><span style="width: 50px;display:inline-block;">enabled</span> - use collection proxy config</li>
<li><span style="width: 50px;display:inline-block;">disable</span> - disable proxy</li>
</ul>
</div>
</InfoTip>
`}
infotipId="request-var"
/>
</label>
<div className="flex items-center">
<label className="flex items-center">

View File

@@ -89,7 +89,7 @@ const VarsTable = ({ collection, vars, varType }) => {
<td>
<div className="flex items-center">
<span>Expr</span>
<InfoTip content="You can write any valid JS Template Literal here" infotipId="request-var" />
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
</div>
</td>
)}

View File

@@ -6,9 +6,10 @@ import { IconX } from '@tabler/icons';
import { isWindowsOS } from 'utils/common/platform';
import slash from 'utils/common/slash';
const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false }) => {
const FilePickerEditor = ({ value, onChange, collection }) => {
value = value || [];
const dispatch = useDispatch();
const filenames = (isSingleFilePicker ? [value] : value || [])
const filenames = value
.filter((v) => v != null && v != '')
.map((v) => {
const separator = isWindowsOS() ? '\\' : '/';
@@ -19,7 +20,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
const title = filenames.map((v) => `- ${v}`).join('\n');
const browse = () => {
dispatch(browseFiles([], [!isSingleFilePicker ? "multiSelections": ""]))
dispatch(browseFiles())
.then((filePaths) => {
// If file is in the collection's directory, then we use relative path
// Otherwise, we use the absolute path
@@ -33,7 +34,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
return filePath;
});
onChange(isSingleFilePicker ? filePaths[0] : filePaths);
onChange(filePaths);
})
.catch((error) => {
console.error(error);
@@ -41,14 +42,14 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
};
const clear = () => {
onChange(isSingleFilePicker ? '' : []);
onChange([]);
};
const renderButtonText = (filenames) => {
if (filenames.length == 1) {
return filenames[0];
}
return filenames.length + ' file(s) selected';
return filenames.length + ' files selected';
};
return filenames.length > 0 ? (
@@ -65,9 +66,9 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
</div>
) : (
<button className="btn btn-secondary px-1" style={{ width: '100%' }} onClick={browse}>
{isSingleFilePicker ? 'Select File' : 'Select Files'}
Select Files
</button>
);
};
export default FilePickerEditor;
export default FilePickerEditor;

View File

@@ -88,7 +88,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
<td>
<div className="flex items-center">
<span>Expr</span>
<InfoTip content="You can write any valid JS expression here" infotipId="response-var" />
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
</div>
</td>
)}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Tooltip as ReactInfoTip } from 'react-tooltip';
const InfoTip = ({ html: _ignored, infotipId, ...props }) => {
const InfoTip = ({ text, infotipId }) => {
return (
<>
<svg
@@ -17,7 +17,7 @@ const InfoTip = ({ html: _ignored, infotipId, ...props }) => {
<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" />
</svg>
<ReactInfoTip anchorId={infotipId} {...props} />
<ReactInfoTip anchorId={infotipId} html={text} />
</>
);
};

View File

@@ -1,65 +0,0 @@
import styled from 'styled-components';
const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(2) {
width: 45%;
}
&:nth-child(3) {
width: 25%;
}
&:nth-child(4) {
width: 70px;
}
}
}
.btn-add-param {
font-size: 0.8125rem;
}
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
color: ${(props) => props.theme.table.input.color};
background: transparent;
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type='radio'] {
cursor: pointer;
position: relative;
top: 1px;
}
`;
export default Wrapper;

View File

@@ -1,164 +0,0 @@
import React, { useState, useEffect } from 'react';
import { get, cloneDeep, isArray } from 'lodash';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { addFile as _addFile, updateFile, deleteFile } from 'providers/ReduxStore/slices/collections/index';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import FilePickerEditor from 'components/FilePickerEditor/index';
import SingleLineEditor from 'components/SingleLineEditor/index';
const FileBody = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const params = item.draft ? get(item, 'draft.request.body.file') : get(item, 'request.body.file');
const [enabledFileUid, setEnableFileUid] = useState(params && params.length ? params[0].uid : '');
const addFile = () => {
dispatch(
_addFile({
itemUid: item.uid,
collectionUid: collection.uid,
})
);
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleParamChange = (e, _param, type) => {
const param = cloneDeep(_param);
switch (type) {
case 'filePath': {
param.filePath = e.target.filePath;
param.contentType = "";
break;
}
case 'contentType': {
param.contentType = e.target.contentType;
break;
}
case 'selected': {
param.selected = e.target.selected;
setEnableFileUid(param.uid)
break;
}
}
dispatch(
updateFile({
param: param,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveParams = (param) => {
dispatch(
deleteFile({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>
<div className="flex items-center justify-center">File</div>
</td>
<td>
<div className="flex items-center justify-center">Content-Type</div>
</td>
<td>
<div className="flex items-center justify-center">Selected</div>
</td>
<td></td>
</tr>
</thead>
<tbody>
{params && params.length
? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<FilePickerEditor
isSingleFilePicker={true}
value={param.filePath}
onChange={(path) =>
handleParamChange(
{
target: {
filePath: path
}
},
param,
'filePath'
)
}
collection={collection}
/>
</td>
<td>
<SingleLineEditor
className="flex items-center justify-center"
onSave={onSave}
theme={storedTheme}
placeholder="Auto"
value={param.contentType}
onChange={(newValue) =>
handleParamChange(
{
target: {
contentType: newValue
}
},
param,
'contentType'
)
}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center justify-center">
<input
key={param.uid}
type="radio"
name="selected"
checked={enabledFileUid === param.uid || param.selected}
tabIndex="-1"
className="mr-1 mousetrap"
onChange={(e) => handleParamChange(e, param, 'selected')}
/>
</div>
</td>
<td>
<div className="flex items-center justify-center">
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<div>
<button className="btn-add-param text-link pr-2 pt-3 select-none" onClick={addFile}>
+ Add File
</button>
</div>
</StyledWrapper>
);
};
export default FileBody;

View File

@@ -154,7 +154,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
</div>
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
</div>
<section className="flex w-full mt-5 flex-1 relative">{getTabPanel(focusedTab.requestPaneTab)}</section>
<section className="flex w-full mt-5 flex-1">{getTabPanel(focusedTab.requestPaneTab)}</section>
</StyledWrapper>
);
};

View File

@@ -49,7 +49,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
<>
<StyledWrapper className="w-full relative">
<button
className="btn-add-param text-link px-4 py-4 select-none absolute top-0 right-0 z-10"
onClick={onPrettify}
@@ -68,7 +68,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
onRun={onRun}
onSave={onSave}
/>
</>
</StyledWrapper>
);
};

View File

@@ -176,7 +176,8 @@ const QueryParams = ({ item, collection }) => {
</button>
<div className="mb-2 title text-xs flex items-stretch">
<span>Path</span>
<InfoTip infotipId="path-param-InfoTip">
<InfoTip
text={`
<div>
Path variables are automatically added whenever the
<code className="font-mono mx-2">:name</code>
@@ -185,7 +186,9 @@ const QueryParams = ({ item, collection }) => {
https://example.com/v1/users/<span>:id</span>
</code>
</div>
</InfoTip>
`}
infotipId="path-param-InfoTip"
/>
</div>
<table>
<thead>

View File

@@ -128,15 +128,6 @@ const RequestBodyMode = ({ item, collection }) => {
SPARQL
</div>
<div className="label-item font-medium">Other</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('file');
}}
>
File / Binary
</div>
<div
className="dropdown-item"
onClick={() => {

View File

@@ -8,7 +8,6 @@ import { useTheme } from 'providers/Theme';
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import FileBody from '../FileBody/index';
const RequestBody = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -63,10 +62,6 @@ const RequestBody = ({ item, collection }) => {
);
}
if (bodyMode === 'file') {
return <FileBody item={item} collection={collection}/>
}
if (bodyMode === 'formUrlEncoded') {
return <FormUrlEncodedParams item={item} collection={collection} />;
}
@@ -77,4 +72,4 @@ const RequestBody = ({ item, collection }) => {
return <StyledWrapper className="w-full">No Body</StyledWrapper>;
};
export default RequestBody;
export default RequestBody;

View File

@@ -0,0 +1,10 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
/* todo: find a better way */
height: calc(100vh - 220px);
}
`;
export default StyledWrapper;

View File

@@ -5,6 +5,7 @@ import CodeEditor from 'components/CodeEditor';
import { updateRequestTests } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const Tests = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -27,17 +28,19 @@ const Tests = ({ item, collection }) => {
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
<CodeEditor
collection={collection}
value={tests || ''}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
onEdit={onEdit}
mode="javascript"
onRun={onRun}
onSave={onSave}
/>
<StyledWrapper className="w-full">
<CodeEditor
collection={collection}
value={tests || ''}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
onEdit={onEdit}
mode="javascript"
onRun={onRun}
onSave={onSave}
/>
</StyledWrapper>
);
};

View File

@@ -98,7 +98,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
) : (
<div className="flex items-center">
<span>Expr</span>
<InfoTip content="You can write any valid JS expression here" infotipId="response-var" />
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
</div>
), accessor: 'value', width: '46%' },
{ name: '', accessor: '', width: '14%' }

View File

@@ -35,7 +35,7 @@ const CollectionToolBar = ({ collection }) => {
const viewCollectionSettings = () => {
dispatch(
addTab({
uid: collection.uid,
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-settings'
})

View File

@@ -2,15 +2,15 @@ import React from 'react';
import CloseTabIcon from './CloseTabIcon';
import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick }) => {
const SpecialTab = ({ handleCloseClick, type, tabName }) => {
const getTabInfo = (type, tabName) => {
switch (type) {
case 'collection-settings': {
return (
<div onDoubleClick={handleDoubleClick} className="flex items-center flex-nowrap overflow-hidden">
<>
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1 leading-6">Collection</span>
</div>
</>
);
}
case 'collection-overview': {
@@ -31,7 +31,7 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick }) => {
}
case 'folder-settings': {
return (
<div onDoubleClick={handleDoubleClick} className="flex items-center flex-nowrap overflow-hidden">
<div className="flex items-center flex-nowrap overflow-hidden">
<IconFolder size={18} strokeWidth={1.5} className="text-yellow-600 min-w-[18px]" />
<span className="ml-1 leading-6 truncate">{tabName || 'Folder'}</span>
</div>

View File

@@ -1,6 +1,6 @@
import React, { useState, useRef, Fragment } from 'react';
import get from 'lodash/get';
import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
@@ -73,13 +73,13 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
return (
<StyledWrapper
className={`flex items-center justify-between tab-container px-1 ${tab.preview ? "italic" : ""}`}
className="flex items-center justify-between tab-container px-1"
onMouseUp={handleMouseUp} // Add middle-click behavior here
>
{tab.type === 'folder-settings' ? (
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} />
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} tabName={folder?.name} />
) : (
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} />
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} />
)}
</StyledWrapper>
);
@@ -144,9 +144,8 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
/>
)}
<div
className={`flex items-baseline tab-label pl-2 ${tab.preview ? "italic" : ""}`}
className="flex items-baseline tab-label pl-2"
onContextMenu={handleRightClick}
onDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))}
onMouseUp={(e) => {
if (!item.draft) return handleMouseUp(e);

View File

@@ -127,7 +127,7 @@ const CloneCollection = ({ onClose, collection }) => {
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
<span className="font-semibold">Folder Name</span>
<InfoTip
content="This folder will be created under the selected location"
text="This folder will be created under the selected location"
infotipId="collection-folder-name-infotip"
/>
</label>

View File

@@ -5,8 +5,8 @@ import classnames from 'classnames';
import { useDrag, useDrop } from 'react-dnd';
import { IconChevronRight, IconDots } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
import { addTab, focusTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import { moveItem, showInFolder, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
import { moveItem, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
import Dropdown from 'components/Dropdown';
import NewRequest from 'components/Sidebar/NewRequest';
@@ -23,16 +23,13 @@ import { hideHomePage } from 'providers/ReduxStore/slices/app';
import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
import NetworkError from 'components/ResponsePane/NetworkError/index';
import { findItemInCollection } from 'utils/collections';
import CollectionItemIcon from './CollectionItemIcon';
import { scrollToTheActiveTab } from 'utils/tabs';
import CollectionItemIcon from './CollectionItemIcon/index';
const CollectionItem = ({ item, collection, searchText }) => {
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isSidebarDragging = useSelector((state) => state.app.isDragging);
const dispatch = useDispatch();
const collectionItemRef = useRef(null);
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
@@ -46,31 +43,28 @@ const CollectionItem = ({ item, collection, searchText }) => {
const itemIsCollapsed = hasSearchText ? false : item.collapsed;
const [{ isDragging }, drag] = useDrag({
type: `collection-item-${collection.uid}`,
type: `COLLECTION_ITEM_${collection.uid}`,
item: item,
collect: (monitor) => ({
isDragging: monitor.isDragging()
}),
options: {
dropEffect: "move"
}
})
});
const [{ isOver }, drop] = useDrop({
accept: `collection-item-${collection.uid}`,
accept: `COLLECTION_ITEM_${collection.uid}`,
drop: (draggedItem) => {
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
if (draggedItem.uid !== item.uid) {
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
}
},
canDrop: (draggedItem) => {
return draggedItem.uid !== item.uid;
},
collect: (monitor) => ({
isOver: monitor.isOver(),
}),
isOver: monitor.isOver()
})
});
drag(drop(collectionItemRef));
const dropdownTippyRef = useRef();
const MenuIcon = forwardRef((props, ref) => {
return (
@@ -89,6 +83,13 @@ const CollectionItem = ({ item, collection, searchText }) => {
'item-hovered': isOver
});
const scrollToTheActiveTab = () => {
const activeTab = document.querySelector('.request-tab.active');
if (activeTab) {
activeTab.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid)).catch((err) =>
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
@@ -98,13 +99,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
};
const handleClick = (event) => {
if (event.detail != 1) return;
//scroll to the active tab
setTimeout(scrollToTheActiveTab, 50);
const isRequest = isItemARequest(item);
if (isRequest) {
if (isItemARequest(item)) {
dispatch(hideHomePage());
if (itemIsOpenedInTabs(item, tabs)) {
dispatch(
@@ -114,21 +112,20 @@ const CollectionItem = ({ item, collection, searchText }) => {
);
return;
}
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
requestPaneTab: getDefaultRequestPaneTab(item),
type: 'request',
requestPaneTab: getDefaultRequestPaneTab(item)
})
);
} else {
return;
}
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
type: 'folder-settings',
type: 'folder-settings'
})
);
dispatch(
@@ -137,12 +134,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
collectionUid: collection.uid
})
);
}
};
const handleFolderCollapse = (e) => {
e.stopPropagation();
e.preventDefault();
const handleFolderCollapse = () => {
dispatch(
collectionFolderClicked({
itemUid: item.uid,
@@ -162,6 +156,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
}
};
const handleDoubleClick = (event) => {
setRenameItemModalOpen(true);
};
let indents = range(item.depth);
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const isFolder = isItemAFolder(item);
@@ -182,10 +180,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
}
}
const handleDoubleClick = (event) => {
dispatch(makeTabPermanent({ uid: item.uid }))
};
// we need to sort request items by seq property
const sortRequestItems = (items = []) => {
return items.sort((a, b) => a.seq - b.seq);
@@ -226,13 +220,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
}
};
const handleShowInFolder = () => {
dispatch(showInFolder(item.pathname)).catch((error) => {
console.error('Error opening the folder', error);
toast.error('Error opening the folder');
});
};
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
@@ -259,7 +246,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
{generateCodeItemModalOpen && (
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
)}
<div className={itemRowClassName} ref={collectionItemRef}>
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
<div className="flex items-center h-full w-full">
{indents && indents.length
? indents.map((i) => {
@@ -286,9 +273,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
style={{
paddingLeft: 8
}}
onClick={handleClick}
onContextMenu={handleRightClick}
onDoubleClick={handleDoubleClick}
>
<div style={{ width: 16, minWidth: 16 }}>
{isFolder ? (
@@ -304,6 +288,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
<div
className="ml-1 flex w-full h-full items-center overflow-hidden"
onClick={handleClick}
onContextMenu={handleRightClick}
onDoubleClick={handleDoubleClick}
>
<CollectionItemIcon item={item} />
<span className="item-name" title={item.name}>
@@ -384,15 +371,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
Generate Code
</div>
)}
<div
className="dropdown-item"
onClick={(e) => {
dropdownTippyRef.current.hide();
handleShowInFolder();
}}
>
Show in Folder
</div>
<div
className="dropdown-item delete-item"
onClick={(e) => {

View File

@@ -12,17 +12,6 @@ const Wrapper = styled.div`
transform: rotateZ(90deg);
}
&.item-hovered {
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
.collection-actions {
.dropdown {
div[aria-expanded='false'] {
visibility: visible;
}
}
}
}
.collection-actions {
.dropdown {
div[aria-expanded='true'] {

View File

@@ -2,13 +2,13 @@ import React, { useState, forwardRef, useRef, useEffect } from 'react';
import classnames from 'classnames';
import { uuid } from 'utils/common';
import filter from 'lodash/filter';
import { useDrop, useDrag } from 'react-dnd';
import { useDrop } from 'react-dnd';
import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { collapseCollection } from 'providers/ReduxStore/slices/collections';
import { mountCollection, moveItemToRootOfCollection, moveCollectionAndPersist } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch, useSelector } from 'react-redux';
import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import { mountCollection, moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder';
import CollectionItem from './CollectionItem';
@@ -20,8 +20,7 @@ import { isItemAFolder, isItemARequest } from 'utils/collections';
import RenameCollection from './RenameCollection';
import StyledWrapper from './StyledWrapper';
import CloneCollection from './CloneCollection';
import { areItemsLoading, findItemInCollection } from 'utils/collections';
import { scrollToTheActiveTab } from 'utils/tabs';
import { areItemsLoading } from 'utils/collections';
const Collection = ({ collection, searchText }) => {
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
@@ -30,10 +29,8 @@ const Collection = ({ collection, searchText }) => {
const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false);
const [showExportCollectionModal, setShowExportCollectionModal] = useState(false);
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
const tabs = useSelector((state) => state.tabs.tabs);
const dispatch = useDispatch();
const isLoading = areItemsLoading(collection);
const collectionRef = useRef(null);
const menuDropdownTippyRef = useRef();
const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);
@@ -55,16 +52,6 @@ const Collection = ({ collection, searchText }) => {
);
};
const ensureCollectionIsMounted = () => {
if (collection.mountStatus === 'unmounted') {
dispatch(mountCollection({
collectionUid: collection.uid,
collectionPathname: collection.pathname,
brunoConfig: collection.brunoConfig
}));
}
}
const hasSearchText = searchText && searchText?.trim()?.length;
const collectionIsCollapsed = hasSearchText ? false : collection.collapsed;
@@ -73,37 +60,30 @@ const Collection = ({ collection, searchText }) => {
});
const handleClick = (event) => {
if (event.detail != 1) return;
// Check if the click came from the chevron icon
const isChevronClick = event.target.closest('svg')?.classList.contains('chevron-icon');
setTimeout(scrollToTheActiveTab, 50);
ensureCollectionIsMounted();
if (collection.mountStatus === 'unmounted') {
dispatch(mountCollection({
collectionUid: collection.uid,
collectionPathname: collection.pathname,
brunoConfig: collection.brunoConfig
}));
}
dispatch(collapseCollection(collection.uid));
// Only open collection settings if not clicking the chevron
if(!isChevronClick) {
dispatch(
addTab({
uid: collection.uid,
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-settings',
type: 'collection-settings'
})
);
}
};
const handleDoubleClick = (event) => {
dispatch(makeTabPermanent({ uid: collection.uid }))
};
const handleCollectionCollapse = (e) => {
e.stopPropagation();
e.preventDefault();
ensureCollectionIsMounted();
dispatch(collapseCollection(collection.uid));
}
const handleRightClick = (event) => {
const _menuDropdown = menuDropdownTippyRef.current;
if (_menuDropdown) {
@@ -118,58 +98,33 @@ const Collection = ({ collection, searchText }) => {
const viewCollectionSettings = () => {
dispatch(
addTab({
uid: collection.uid,
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-settings'
})
);
};
const isCollectionItem = (itemType) => {
return itemType.startsWith('collection-item');
};
const [{ isDragging }, drag] = useDrag({
type: "collection",
item: collection,
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
options: {
dropEffect: "move"
}
});
const [{ isOver }, drop] = useDrop({
accept: ["collection", `collection-item-${collection.uid}`],
drop: (draggedItem, monitor) => {
const itemType = monitor.getItemType();
if (isCollectionItem(itemType)) {
dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid))
} else {
dispatch(moveCollectionAndPersist({draggedItem, targetItem: collection}));
}
accept: `COLLECTION_ITEM_${collection.uid}`,
drop: (draggedItem) => {
dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid));
},
canDrop: (draggedItem) => {
return draggedItem.uid !== collection.uid;
// todo need to make sure that draggedItem belongs to the collection
return true;
},
collect: (monitor) => ({
isOver: monitor.isOver(),
}),
isOver: monitor.isOver()
})
});
drag(drop(collectionRef));
if (searchText && searchText.length) {
if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) {
return null;
}
}
const collectionRowClassName = classnames('flex py-1 collection-name items-center', {
'item-hovered': isOver
});
// we need to sort request items by seq property
const sortRequestItems = (items = []) => {
return items.sort((a, b) => a.seq - b.seq);
@@ -199,13 +154,10 @@ const Collection = ({ collection, searchText }) => {
{showCloneCollectionModalOpen && (
<CloneCollection collection={collection} onClose={() => setShowCloneCollectionModalOpen(false)} />
)}
<div className={collectionRowClassName}
ref={collectionRef}
>
<div className="flex py-1 collection-name items-center" ref={drop}>
<div
className="flex flex-grow items-center overflow-hidden"
onClick={handleClick}
onDoubleClick={handleDoubleClick}
onContextMenu={handleRightClick}
>
<IconChevronRight
@@ -213,7 +165,6 @@ const Collection = ({ collection, searchText }) => {
strokeWidth={2}
className={`chevron-icon ${iconClassName}`}
style={{ width: 16, minWidth: 16, color: 'rgb(160 160 160)' }}
onClick={handleCollectionCollapse}
/>
<div className="ml-1" id="sidebar-collection-name">
{collection.name}

View File

@@ -8,10 +8,12 @@ import {
IconSortDescendingLetters,
IconX
} from '@tabler/icons';
import Collection from './Collection';
import Collection from '../Collections/Collection';
import CreateCollection from '../CreateCollection';
import StyledWrapper from './StyledWrapper';
import CreateOrOpenCollection from './CreateOrOpenCollection';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { sortCollections } from 'providers/ReduxStore/slices/collections/actions';
// todo: move this to a separate folder
@@ -117,7 +119,9 @@ const Collections = () => {
{collections && collections.length
? collections.map((c) => {
return (
<Collection searchText={searchText} collection={c} key={c.uid} />
<DndProvider backend={HTML5Backend} key={c.uid}>
<Collection searchText={searchText} collection={c} key={c.uid} />
</DndProvider>
);
})
: null}

View File

@@ -120,7 +120,7 @@ const CreateCollection = ({ onClose }) => {
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
<span className="font-semibold">Folder Name</span>
<InfoTip
content="This folder will be created under the selected location"
text="This folder will be created under the selected location"
infotipId="collection-folder-name-infotip"
/>
</label>

View File

@@ -34,7 +34,7 @@ const ToolHint = ({
<StyledWrapper theme={appliedTheme}>
<ReactToolHint
anchorId={toolhintId}
content={text}
html={text}
className="toolhint"
offset={offset}
place={place}

View File

@@ -1,8 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './pages/index';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
const rootElement = document.getElementById('root');
@@ -10,9 +8,7 @@ if (rootElement) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<DndProvider backend={HTML5Backend}>
<App />
</DndProvider>
<App />
</React.StrictMode>
);
}

View File

@@ -6,13 +6,12 @@ import collectionsReducer from './slices/collections';
import tabsReducer from './slices/tabs';
import notificationsReducer from './slices/notifications';
import globalEnvironmentsReducer from './slices/global-environments';
import { draftDetectMiddleware } from './middlewares/draft/middleware';
const isDevEnv = () => {
return import.meta.env.MODE === 'development';
};
let middleware = [tasksMiddleware.middleware, draftDetectMiddleware];
let middleware = [tasksMiddleware.middleware];
if (isDevEnv()) {
middleware = [...middleware, debugMiddleware.middleware];
}

View File

@@ -1,56 +0,0 @@
import { handleMakeTabParmanent } from "./utils";
const actionsToIntercept = [
'collections/requestUrlChanged',
'collections/updateAuth',
'collections/addQueryParam',
'collections/moveQueryParam',
'collections/updateQueryParam',
'collections/deleteQueryParam',
'collections/updatePathParam',
'collections/addRequestHeader',
'collections/updateRequestHeader',
'collections/deleteRequestHeader',
'collections/moveRequestHeader',
'collections/addFormUrlEncodedParam',
'collections/updateFormUrlEncodedParam',
'collections/deleteFormUrlEncodedParam',
'collections/moveFormUrlEncodedParam',
'collections/addMultipartFormParam',
'collections/updateMultipartFormParam',
'collections/deleteMultipartFormParam',
'collections/moveMultipartFormParam',
'collections/updateRequestAuthMode',
'collections/updateRequestBodyMode',
'collections/updateRequestBody',
'collections/updateRequestGraphqlQuery',
'collections/updateRequestGraphqlVariables',
'collections/updateRequestScript',
'collections/updateResponseScript',
'collections/updateRequestTests',
'collections/updateRequestMethod',
'collections/addAssertion',
'collections/updateAssertion',
'collections/deleteAssertion',
'collections/moveAssertion',
'collections/addVar',
'collections/updateVar',
'collections/deleteVar',
'collections/moveVar',
'collections/addFolderHeader',
'collections/updateFolderHeader',
'collections/deleteFolderHeader',
'collections/addFolderVar',
'collections/updateFolderVar',
'collections/deleteFolderVar',
'collections/updateRequestDocs',
'collections/runRequestEvent', // TODO: This doesn't necessarily related to a draft state, need to rethink.
];
export const draftDetectMiddleware = ({ dispatch, getState }) => (next) => (action) => {
if (actionsToIntercept.includes(action.type)) {
const state = getState();
handleMakeTabParmanent(state, action, dispatch);
}
return next(action);
};

View File

@@ -1,21 +0,0 @@
import { makeTabPermanent } from "providers/ReduxStore/slices/tabs";
import { findCollectionByUid, findItemInCollection } from "utils/collections/index";
import find from 'lodash/find';
function handleMakeTabParmanent(state, action, dispatch) {
const tabs = state.tabs.tabs;
const activeTabUid = state.tabs.activeTabUid;
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
const itemUid = action.payload.itemUid || action.payload.folderUid
const collection = findCollectionByUid(state.collections.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, itemUid);
if (item && focusedTab.preview == true) {
dispatch(makeTabPermanent({ uid: itemUid }));
}
}
}
export {
handleMakeTabParmanent
}

View File

@@ -21,7 +21,7 @@ import {
transformRequestToSaveToFilesystem
} from 'utils/collections';
import { uuid, waitForNextTick } from 'utils/common';
import { PATH_SEPARATOR, getDirectoryName, isWindowsPath } from 'utils/common/platform';
import { PATH_SEPARATOR, getDirectoryName } from 'utils/common/platform';
import { cancelNetworkRequest, sendNetworkRequest } from 'utils/network';
import { callIpc } from 'utils/common/ipc';
@@ -32,7 +32,6 @@ import {
selectEnvironment as _selectEnvironment,
sortCollections as _sortCollections,
updateCollectionMountStatus,
moveCollection,
requestCancelled,
resetRunResults,
responseReceived,
@@ -494,7 +493,7 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
);
if (!reqWithSameNameExists) {
const dirname = getDirectoryName(item.pathname);
const fullName = isWindowsPath(item.pathname) ? path.win32.join(dirname, filename) : path.join(dirname, filename);
const fullName = path.join(dirname, filename);
const { ipcRenderer } = window;
const requestItems = filter(parentItem.items, (i) => i.type !== 'folder');
itemToSave.seq = requestItems ? requestItems.length + 1 : 1;
@@ -759,8 +758,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
xml: null,
sparql: null,
multipartForm: null,
formUrlEncoded: null,
file: null
formUrlEncoded: null
},
auth: auth ?? {
mode: 'none'
@@ -1040,17 +1038,14 @@ export const browseDirectory = () => (dispatch, getState) => {
};
export const browseFiles =
(filters, properties) =>
(_dispatch, _getState) => {
(filters = []) =>
(dispatch, getState) => {
const { ipcRenderer } = window;
return new Promise((resolve, reject) => {
ipcRenderer
.invoke('renderer:browse-files', filters, properties)
.then(resolve)
.catch(reject);
ipcRenderer.invoke('renderer:browse-files', filters).then(resolve).catch(reject);
});
};
};
export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getState) => {
const state = getState();
@@ -1152,22 +1147,6 @@ export const importCollection = (collection, collectionLocation) => (dispatch, g
});
};
export const moveCollectionAndPersist = ({ draggedItem, targetItem }) => (dispatch, getState) => {
dispatch(moveCollection({ draggedItem, targetItem }));
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
const state = getState();
const collectionPaths = state.collections.collections.map((collection) => collection.pathname);
ipcRenderer
.invoke('renderer:update-collection-paths', collectionPaths)
.then(resolve)
.catch(reject);
});
};
export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
@@ -1240,10 +1219,3 @@ export const mountCollection = ({ collectionUid, collectionPathname, brunoConfig
});
});
};
export const showInFolder = (collectionPath) => () => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:show-in-folder', collectionPath).then(resolve).catch(reject);
});
};

View File

@@ -1,5 +1,5 @@
import { uuid } from 'utils/common';
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, findIndex } from 'lodash';
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set } from 'lodash';
import { createSlice } from '@reduxjs/toolkit';
import {
addDepth,
@@ -18,8 +18,6 @@ import {
import { parsePathParams, parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url';
import { getDirectoryName, getSubdirectoriesFromRoot, PATH_SEPARATOR } from 'utils/common/platform';
import toast from 'react-hot-toast';
import mime from 'mime-types';
import path from 'node:path';
const initialState = {
collections: [],
@@ -100,12 +98,6 @@ export const collectionsSlice = createSlice({
break;
}
},
moveCollection: (state, action) => {
const { draggedItem, targetItem } = action.payload;
state.collections = state.collections.filter((i) => i.uid !== draggedItem.uid); // Remove dragged item
const targetItemIndex = state.collections.findIndex((i) => i.uid === targetItem.uid); // Find target item
state.collections.splice(targetItemIndex, 0, draggedItem); // Insert dragged-item above target-item
},
updateLastAction: (state, action) => {
const { collectionUid, lastAction } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -903,76 +895,6 @@ export const collectionsSlice = createSlice({
}
}
},
addFile: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
if (!item.draft) {
item.draft = cloneDeep(item);
}
item.draft.request.body.file = item.draft.request.body.file || [];
item.draft.request.body.file.push({
uid: uuid(),
filePath: '',
contentType: '',
selected: false
});
}
}
},
updateFile: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
if (!item.draft) {
item.draft = cloneDeep(item);
}
const param = find(item.draft.request.body.file, (p) => p.uid === action.payload.param.uid);
if (param) {
const contentType = mime.contentType(path.extname(action.payload.param.filePath));
param.filePath = action.payload.param.filePath;
param.contentType = action.payload.param.contentType || contentType || '';
param.selected = action.payload.param.selected;
item.draft.request.body.file = item.draft.request.body.file.map((p) => {
p.selected = p.uid === param.uid;
return p;
});
}
}
}
},
deleteFile: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
if (!item.draft) {
item.draft = cloneDeep(item);
}
item.draft.request.body.file = filter(
item.draft.request.body.file,
(p) => p.uid !== action.payload.paramUid
);
if (item.draft.request.body.file.length > 0) {
item.draft.request.body.file[0].selected = true;
}
}
}
},
updateRequestAuthMode: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -1029,10 +951,6 @@ export const collectionsSlice = createSlice({
item.draft.request.body.sparql = action.payload.content;
break;
}
case 'file': {
item.draft.request.body.file = action.payload.content;
break;
}
case 'formUrlEncoded': {
item.draft.request.body.formUrlEncoded = action.payload.content;
break;
@@ -1767,12 +1685,6 @@ export const collectionsSlice = createSlice({
// we don't want to lose the draft in this case
if (areItemsTheSameExceptSeqUpdate(item, file.data)) {
item.seq = file.data.seq;
if (item?.draft) {
item.draft.seq = file.data.seq;
}
if (item?.draft && areItemsTheSameExceptSeqUpdate(item?.draft, file.data)) {
item.draft = null;
}
} else {
item.name = file.data.name;
item.type = file.data.type;
@@ -2040,9 +1952,6 @@ export const {
addMultipartFormParam,
updateMultipartFormParam,
deleteMultipartFormParam,
addFile,
updateFile,
deleteFile,
moveMultipartFormParam,
updateRequestAuthMode,
updateRequestBodyMode,
@@ -2094,8 +2003,7 @@ export const {
runFolderEvent,
resetCollectionRunner,
updateRequestDocs,
updateFolderDocs,
moveCollection
updateFolderDocs
} = collectionsSlice.actions;
export default collectionsSlice.reducer;

View File

@@ -1,5 +1,4 @@
import { createSlice } from '@reduxjs/toolkit';
import { findIndex } from 'lodash';
import filter from 'lodash/filter';
import find from 'lodash/find';
import last from 'lodash/last';
@@ -20,59 +19,31 @@ export const tabsSlice = createSlice({
initialState,
reducers: {
addTab: (state, action) => {
const { uid, collectionUid, type, requestPaneTab, preview } = action.payload;
const nonReplaceableTabTypes = [
"variables",
"collection-runner",
"security-settings",
];
const existingTab = find(state.tabs, (tab) => tab.uid === uid);
if (existingTab) {
state.activeTabUid = existingTab.uid;
const alreadyExists = find(state.tabs, (tab) => tab.uid === action.payload.uid);
if (alreadyExists) {
return;
}
if (nonReplaceableTabTypes.includes(type)) {
const existingTab = tabTypeAlreadyExists(state.tabs, collectionUid, type);
if (existingTab) {
state.activeTabUid = existingTab.uid;
if (
['variables', 'collection-settings', 'collection-overview', 'collection-runner', 'security-settings'].includes(action.payload.type)
) {
const tab = tabTypeAlreadyExists(state.tabs, action.payload.collectionUid, action.payload.type);
if (tab) {
state.activeTabUid = tab.uid;
return;
}
}
const lastTab = state.tabs[state.tabs.length - 1];
if (state.tabs.length > 0 && lastTab.preview) {
state.tabs[state.tabs.length - 1] = {
uid,
collectionUid,
requestPaneWidth: null,
requestPaneTab: requestPaneTab || 'params',
responsePaneTab: 'response',
type: type || 'request',
preview: preview !== undefined
? preview
: !nonReplaceableTabTypes.includes(type),
...(uid ? { folderUid: uid } : {})
}
state.activeTabUid = uid;
return
}
state.tabs.push({
uid,
collectionUid,
uid: action.payload.uid,
collectionUid: action.payload.collectionUid,
requestPaneWidth: null,
requestPaneTab: requestPaneTab || 'params',
requestPaneTab: action.payload.requestPaneTab || 'params',
responsePaneTab: 'response',
type: type || 'request',
...(uid ? { folderUid: uid } : {}),
preview: preview !== undefined
? preview
: !nonReplaceableTabTypes.includes(type)
type: action.payload.type || 'request',
...(action.payload.uid ? { folderUid: action.payload.uid } : {})
});
state.activeTabUid = uid;
state.activeTabUid = action.payload.uid;
},
focusTab: (state, action) => {
state.activeTabUid = action.payload.uid;
@@ -153,15 +124,6 @@ export const tabsSlice = createSlice({
const collectionUid = action.payload.collectionUid;
state.tabs = filter(state.tabs, (t) => t.collectionUid !== collectionUid);
state.activeTabUid = null;
},
makeTabPermanent: (state, action) => {
const { uid } = action.payload;
const tab = find(state.tabs, (t) => t.uid === uid);
if (tab) {
tab.preview = false;
} else{
console.error("Tab not found!")
}
}
}
});
@@ -174,8 +136,7 @@ export const {
updateRequestPaneTab,
updateResponsePaneTab,
closeTabs,
closeAllCollectionTabs,
makeTabPermanent
closeAllCollectionTabs
} = tabsSlice.actions;
export default tabsSlice.reducer;
export default tabsSlice.reducer;

View File

@@ -14,8 +14,6 @@ const createContentType = (mode) => {
return 'application/json';
case 'multipartForm':
return 'multipart/form-data';
case 'file':
return 'application/octet-stream';
default:
return '';
}
@@ -62,51 +60,22 @@ const createPostData = (body, type) => {
}
const contentType = createContentType(body.mode);
switch (body.mode) {
case 'formUrlEncoded':
return {
mimeType: contentType,
text: new URLSearchParams(
body[body.mode]
.filter((param) => param.enabled)
.reduce((acc, param) => {
acc[param.name] = param.value;
return acc;
}, {})
).toString(),
params: body[body.mode]
.filter((param) => param.enabled)
.map((param) => ({
name: param.name,
value: param.value
}))
};
case 'multipartForm':
return {
mimeType: contentType,
params: body[body.mode]
.filter((param) => param.enabled)
.map((param) => ({
name: param.name,
value: param.value,
...(param.type === 'file' && { fileName: param.value })
}))
};
case 'file':
return {
mimeType: body[body.mode].filter((param) => param.enabled)[0].contentType,
params: body[body.mode]
.filter((param) => param.selected)
.map((param) => ({
value: param.filePath,
}))
};
default:
return {
mimeType: contentType,
text: body[body.mode]
};
if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') {
return {
mimeType: contentType,
params: body[body.mode]
.filter((param) => param.enabled)
.map((param) => ({
name: param.name,
value: param.value,
...(param.type === 'file' && { fileName: param.value })
}))
};
} else {
return {
mimeType: contentType,
text: body[body.mode]
};
}
};
@@ -120,7 +89,6 @@ export const buildHarRequest = ({ request, headers, type }) => {
queryString: createQuery(request.params),
postData: createPostData(request.body, type),
headersSize: 0,
bodySize: 0,
binary: true
bodySize: 0
};
};

View File

@@ -14,7 +14,6 @@ export const deleteUidsInItems = (items) => {
each(get(item, 'request.vars.assertions'), (a) => delete a.uid);
each(get(item, 'request.body.multipartForm'), (param) => delete param.uid);
each(get(item, 'request.body.formUrlEncoded'), (param) => delete param.uid);
each(get(item, 'request.body.file'), (param) => delete param.uid);
}
if (item.items && item.items.length) {

View File

@@ -1,4 +1,13 @@
import {cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash';
import get from 'lodash/get';
import each from 'lodash/each';
import find from 'lodash/find';
import findIndex from 'lodash/findIndex';
import isString from 'lodash/isString';
import map from 'lodash/map';
import filter from 'lodash/filter';
import sortBy from 'lodash/sortBy';
import isEqual from 'lodash/isEqual';
import cloneDeep from 'lodash/cloneDeep';
import { uuid } from 'utils/common';
import path from 'path';
import slash from 'utils/common/slash';
@@ -272,17 +281,6 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
});
};
const copyFileParams = (params = []) => {
return map(params, (param) => {
return {
uid: param.uid,
filePath: param.filePath,
contentType: param.contentType,
selected: param.selected
}
});
}
const copyItems = (sourceItems, destItems) => {
each(sourceItems, (si) => {
if (!isItemAFolder(si) && !isItemARequest(si) && si.type !== 'js') {
@@ -310,8 +308,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
graphql: si.request.body.graphql,
sparql: si.request.body.sparql,
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
multipartForm: copyMultipartFormParams(si.request.body.multipartForm),
file: copyFileParams(si.request.body.file)
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
},
script: si.request.script,
vars: si.request.vars,
@@ -664,10 +661,6 @@ export const humanizeRequestBodyMode = (mode) => {
label = 'SPARQL';
break;
}
case 'file': {
label = 'File / Binary';
break;
}
case 'formUrlEncoded': {
label = 'Form URL Encoded';
break;
@@ -768,7 +761,6 @@ export const refreshUidsInItem = (item) => {
each(get(item, 'request.params'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.file'), (param) => (param.uid = uuid()));
return item;
};
@@ -779,13 +771,11 @@ export const deleteUidsInItem = (item) => {
const headers = get(item, 'request.headers', []);
const bodyFormUrlEncoded = get(item, 'request.body.formUrlEncoded', []);
const bodyMultipartForm = get(item, 'request.body.multipartForm', []);
const file = get(item, 'request.body.file', []);
params.forEach((param) => delete param.uid);
headers.forEach((header) => delete header.uid);
bodyFormUrlEncoded.forEach((param) => delete param.uid);
bodyMultipartForm.forEach((param) => delete param.uid);
file.forEach((param) => delete param.uid);
return item;
};

View File

@@ -159,8 +159,6 @@ export const getCodeMirrorModeBasedOnContentType = (contentType, body) => {
if (contentType.includes('json')) {
return 'application/ld+json';
} else if (contentType.includes('image')) {
return 'application/image';
} else if (contentType.includes('xml')) {
return 'application/xml';
} else if (contentType.includes('html')) {
@@ -171,6 +169,8 @@ export const getCodeMirrorModeBasedOnContentType = (contentType, body) => {
return 'application/xml';
} else if (contentType.includes('yaml')) {
return 'application/yaml';
} else if (contentType.includes('image')) {
return 'application/image';
} else {
return 'application/text';
}

View File

@@ -94,8 +94,6 @@ export const getContentType = (headers) => {
if (contentType && contentType.length) {
if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(contentType[0])) {
return 'application/ld+json';
} else if (typeof contentType[0] === 'string' && /^image\/svg\+xml/i.test(contentType[0])) {
return 'image/svg+xml';
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
return 'application/xml';
}

View File

@@ -24,25 +24,11 @@ export const getSubdirectoriesFromRoot = (rootPath, pathname) => {
return relativePath ? relativePath.split(path.sep) : [];
};
export const isWindowsPath = (pathname) => {
if (!isWindowsOS()) {
return false;
}
// Check for Windows drive letter format (e.g., "C:\")
const hasDriveLetter = /^[a-zA-Z]:\\/.test(pathname);
// Check for UNC path format (e.g., "\\server\share") a.k.a. network path || WSL path
const isUNCPath = pathname.startsWith('\\\\');
return hasDriveLetter || isUNCPath;
};
export const getDirectoryName = (pathname) => {
return isWindowsPath(pathname) ? path.win32.dirname(pathname) : path.dirname(pathname);
// convert to unix style path
pathname = slash(pathname);
return path.dirname(pathname);
};
export const isWindowsOS = () => {

View File

@@ -9,7 +9,6 @@
import parseCurlCommand from './parse-curl';
import * as querystring from 'query-string';
import * as jsesc from 'jsesc';
import * as path from 'path';
function getContentType(headers = {}) {
const contentType = Object.keys(headers).find((key) => key.toLowerCase() === 'content-type');
@@ -100,29 +99,8 @@ function getMultipleDataString(request, parsedQueryString) {
function getFilesString(request) {
const data = {};
data.data = {};
if (request.isDataBinary) {
let filePath = '';
if (request.data.startsWith('@')) {
filePath = request.data.slice(1);
} else {
filePath = request.data;
}
data.data = [
{
filePath: repr(filePath),
contentType: request.headers['Content-Type'],
selected: true,
}
];
return data;
}
data.files = {};
data.data = {};
for (const multipartKey in request.multipartUploads) {
const multipartValue = request.multipartUploads[multipartKey];
@@ -162,7 +140,6 @@ const curlToJson = (curlCommand) => {
requestJson.url = request.urlWithoutQuery;
requestJson.raw_url = request.url;
requestJson.method = request.method;
requestJson.isDataBinary = request.isDataBinary;
if (request.cookies) {
const cookies = {};
@@ -184,10 +161,12 @@ const curlToJson = (curlCommand) => {
if (request.query) {
requestJson.queries = getQueries(request);
} else if (request.multipartUploads || request.isDataBinary) {
Object.assign(requestJson, getFilesString(request));
} else if (typeof request.data === 'string' || typeof request.data === 'number') {
}
if (typeof request.data === 'string' || typeof request.data === 'number') {
Object.assign(requestJson, getDataString(request));
} else if (request.multipartUploads) {
Object.assign(requestJson, getFilesString(request));
}
if (request.insecure) {

View File

@@ -86,38 +86,4 @@ describe('curlToJson', () => {
method: 'get'
});
});
it('should return a parse a curl with a post body with binary file type', () => {
const curlCommand = `curl 'https://www.usebruno.com'
-H 'Accept: application/json, text/plain, */*'
-H 'Accept-Language: en-US,en;q=0.9,hi;q=0.8'
-H 'Content-Type: application/json;charset=utf-8'
-H 'Origin: https://www.usebruno.com'
-H 'Referer: https://www.usebruno.com/'
--data-binary '@/path/to/file'
`;
const result = curlToJson(curlCommand);
expect(result).toEqual({
url: 'https://www.usebruno.com',
raw_url: 'https://www.usebruno.com',
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8',
'Content-Type': 'application/json;charset=utf-8',
Origin: 'https://www.usebruno.com',
Referer: 'https://www.usebruno.com/'
},
isDataBinary: true,
data: [
{
filePath: '/path/to/file',
contentType: 'application/json;charset=utf-8',
selected: true
}
]
});
});
});

View File

@@ -50,18 +50,14 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque
sparql: null,
multipartForm: null,
formUrlEncoded: null,
graphql: null,
file: null
graphql: null
};
if (parsedBody && contentType && typeof contentType === 'string') {
if (requestType === 'graphql-request' && (contentType.includes('application/json') || contentType.includes('application/graphql'))) {
body.mode = 'graphql';
body.graphql = parseGraphQL(parsedBody);
} else if (requestType === 'http-request' && request.isDataBinary) {
body.mode = 'file';
body.file = parsedBody;
}else if (contentType.includes('application/json')) {
} else if (contentType.includes('application/json')) {
body.mode = 'json';
body.json = convertToCodeMirrorJson(parsedBody);
} else if (contentType.includes('xml')) {

View File

@@ -35,7 +35,6 @@ export const updateUidsInCollection = (_collection) => {
each(get(item, 'request.assertions'), (a) => (a.uid = uuid()));
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.file'), (param) => (param.uid = uuid()));
if (item.items && item.items.length) {
updateItemUids(item.items);

View File

@@ -229,27 +229,26 @@ const transformOpenapiRequestItem = (request) => {
return brunoRequestItem;
};
const resolveRefs = (spec, components = spec?.components, cache = new Map()) => {
const resolveRefs = (spec, components = spec?.components, visitedItems = new Set()) => {
if (!spec || typeof spec !== 'object') {
return spec;
}
if (cache.has(spec)) {
return cache.get(spec);
}
if (Array.isArray(spec)) {
return spec.map(item => resolveRefs(item, components, cache));
return spec.map((item) => resolveRefs(item, components, visitedItems));
}
if ('$ref' in spec) {
const refPath = spec.$ref;
if (cache.has(refPath)) {
return cache.get(refPath);
if (visitedItems.has(refPath)) {
return spec;
} else {
visitedItems.add(refPath);
}
if (refPath.startsWith('#/components/')) {
// Local reference within components
const refKeys = refPath.replace('#/components/', '').split('/');
let ref = components;
@@ -257,26 +256,25 @@ const resolveRefs = (spec, components = spec?.components, cache = new Map()) =>
if (ref && ref[key]) {
ref = ref[key];
} else {
// Handle invalid references gracefully?
return spec;
}
}
cache.set(refPath, {});
const resolved = resolveRefs(ref, components, cache);
cache.set(refPath, resolved);
return resolved;
return resolveRefs(ref, components, visitedItems);
} else {
// Handle external references (not implemented here)
// You would need to fetch the external reference and resolve it.
// Example: Fetch and resolve an external reference from a URL.
}
return spec;
}
const resolved = {};
cache.set(spec, resolved);
for (const [key, value] of Object.entries(spec)) {
resolved[key] = resolveRefs(value, components, cache);
// Recursively resolve references in nested objects
for (const prop in spec) {
spec[prop] = resolveRefs(spec[prop], components, new Set(visitedItems));
}
return resolved;
return spec;
};
const groupRequestsByTags = (requests) => {

View File

@@ -422,7 +422,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
brunoRequestItem.request.auth.mode = 'apikey';
brunoRequestItem.request.auth.apikey = {
key: authValues.key,
value: authValues.value?.toString(), // Convert the value to a string as Postman's schema does not rigidly define the type of it,
value: authValues.value,
placement: "header" //By default we are placing the apikey values in headers!
}
}

View File

@@ -24,10 +24,6 @@ const replacements = {
'postman\\.setEnvironmentVariable\\(': 'bru.setEnvVar(',
'postman\\.getEnvironmentVariable\\(': 'bru.getEnvVar(',
'postman\\.clearEnvironmentVariable\\(': 'bru.deleteEnvVar(',
'pm\\.execution\\.skipRequest\\(\\)': 'bru.runner.skipRequest()',
'pm\\.execution\\.skipRequest': 'bru.runner.skipRequest',
'pm\\.execution\\.setNextRequest\\(null\\)': 'bru.runner.stopExecution()',
'pm\\.execution\\.setNextRequest\\(\'null\'\\)': 'bru.runner.stopExecution()',
};
const extendedReplacements = Object.keys(replacements).reduce((acc, key) => {
@@ -54,7 +50,7 @@ export const postmanTranslation = (script, logCallback) => {
}
if (modifiedScript.includes('pm.') || modifiedScript.includes('postman.')) {
modifiedScript = modifiedScript.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1');
//logCallback?.();
logCallback?.();
}
return modifiedScript;
} catch (e) {

View File

@@ -11,10 +11,3 @@ export const isItemAFolder = (item) => {
export const itemIsOpenedInTabs = (item, tabs) => {
return find(tabs, (t) => t.uid === item.uid);
};
export const scrollToTheActiveTab = () => {
const activeTab = document.querySelector('.request-tab.active');
if (activeTab) {
activeTab.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};

View File

@@ -52,7 +52,7 @@
"@usebruno/lang": "0.12.0",
"@usebruno/vm2": "^3.9.13",
"aws4-axios": "^3.3.0",
"axios": "1.7.7",
"axios": "1.7.5",
"axios-ntlm": "^1.4.2",
"chai": "^4.3.7",
"chalk": "^3.0.0",
@@ -66,6 +66,7 @@
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"tough-cookie": "^4.1.3",
"@usebruno/vm2": "^3.9.13",
"xmlbuilder": "^15.1.1",
"yargs": "^17.6.2"
}

View File

@@ -11,7 +11,6 @@ const { rpad } = require('../utils/common');
const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru');
const { dotenvToJson } = require('@usebruno/lang');
const constants = require('../constants');
const { findItemInCollection } = require('../utils/collection');
const command = 'run [filename]';
const desc = 'Run a request';
@@ -19,7 +18,6 @@ const printRunSummary = (results) => {
let totalRequests = 0;
let passedRequests = 0;
let failedRequests = 0;
let skippedRequests = 0;
let totalAssertions = 0;
let passedAssertions = 0;
let failedAssertions = 0;
@@ -51,10 +49,7 @@ const printRunSummary = (results) => {
failedAssertions += 1;
}
}
if (!hasAnyTestsOrAssertions && result.skipped) {
skippedRequests += 1;
}
else if (!hasAnyTestsOrAssertions && result.error) {
if (!hasAnyTestsOrAssertions && result.error) {
failedRequests += 1;
} else {
passedRequests += 1;
@@ -67,9 +62,6 @@ const printRunSummary = (results) => {
if (failedRequests > 0) {
requestSummary += `, ${chalk.red(`${failedRequests} failed`)}`;
}
if (skippedRequests > 0) {
requestSummary += `, ${chalk.magenta(`${skippedRequests} skipped`)}`;
}
requestSummary += `, ${totalRequests} total`;
let assertSummary = `${rpad('Tests:', maxLength)} ${chalk.green(`${passedTests} passed`)}`;
@@ -92,7 +84,6 @@ const printRunSummary = (results) => {
totalRequests,
passedRequests,
failedRequests,
skippedRequests,
totalAssertions,
passedAssertions,
failedAssertions,
@@ -153,7 +144,7 @@ const createCollectionFromPath = (collectionPath) => {
});
}
}
return currentDirItems;
return currentDirItems
};
collection.items = traverse(collectionPath);
return collection;
@@ -643,34 +634,6 @@ const handler = async function (argv) {
}
const runtime = getJsSandboxRuntime(sandbox);
const runSingleRequestByPathname = async (relativeItemPathname) => {
return new Promise(async (resolve, reject) => {
let itemPathname = path.join(collectionPath, relativeItemPathname);
if (itemPathname && !itemPathname?.endsWith('.bru')) {
itemPathname = `${itemPathname}.bru`;
}
const bruJson = cloneDeep(findItemInCollection(collection, itemPathname));
if (bruJson) {
const res = await runSingleRequest(
itemPathname,
bruJson,
collectionPath,
runtimeVariables,
envVars,
processEnvVars,
brunoConfig,
collectionRoot,
runtime,
collection,
runSingleRequestByPathname
);
resolve(res?.response);
}
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
});
}
let currentRequestIndex = 0;
let nJumps = 0; // count the number of jumps to avoid infinite loops
while (currentRequestIndex < bruJsons.length) {
@@ -688,8 +651,7 @@ const handler = async function (argv) {
brunoConfig,
collectionRoot,
runtime,
collection,
runSingleRequestByPathname
collection
);
results.push({
@@ -739,11 +701,6 @@ const handler = async function (argv) {
// determine next request
const nextRequestName = result?.nextRequestName;
if (result?.shouldStopRunnerExecution) {
break;
}
if (nextRequestName !== undefined) {
nJumps++;
if (nJumps > 10000) {

View File

@@ -62,10 +62,7 @@ const makeJUnitOutput = async (results, outputPath) => {
suite.testcase.push(testcase);
});
if (result?.skipped) {
suite['@skipped'] = 1;
}
else if (result.error) {
if (result.error) {
suite['@errors'] = 1;
suite['@tests'] = 1;
suite.testcase = [

View File

@@ -40,13 +40,11 @@ const runSingleRequest = async function (
brunoConfig,
collectionRoot,
runtime,
collection,
runSingleRequestByPathname
collection
) {
try {
let request;
let nextRequestName;
let shouldStopRunnerExecution = false;
let item = {
pathname: path.join(collectionPath, filename),
...bruJson
@@ -70,41 +68,11 @@ const runSingleRequest = async function (
collectionPath,
onConsoleLog,
processEnvVars,
scriptingConfig,
runSingleRequestByPathname
scriptingConfig
);
if (result?.nextRequestName !== undefined) {
nextRequestName = result.nextRequestName;
}
if (result?.stopExecution) {
shouldStopRunnerExecution = true;
}
if (result?.skipRequest) {
return {
test: {
filename: filename
},
request: {
method: request.method,
url: request.url,
headers: request.headers,
data: request.data
},
response: {
status: 'skipped',
statusText: 'request skipped via pre-request script',
data: null,
responseTime: 0
},
error: 'Request has been skipped from pre-request script',
skipped: true,
assertionResults: [],
testResults: [],
shouldStopRunnerExecution
};
}
}
// interpolate variables inside request
@@ -265,30 +233,7 @@ const runSingleRequest = async function (
if (!options.disableCookies) {
const cookieString = getCookieStringForUrl(request.url);
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
const existingCookieHeaderName = Object.keys(request.headers).find(
name => name.toLowerCase() === 'cookie'
);
const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : '';
// Helper function to parse cookies into an object
const parseCookies = (str) => str.split(';').reduce((cookies, cookie) => {
const [name, ...rest] = cookie.split('=');
if (name && name.trim()) {
cookies[name.trim()] = rest.join('=').trim();
}
return cookies;
}, {});
const mergedCookies = {
...parseCookies(existingCookieString),
...parseCookies(cookieString),
};
const combinedCookieString = Object.entries(mergedCookies)
.map(([name, value]) => `${name}=${value}`)
.join('; ');
request.headers[existingCookieHeaderName || 'Cookie'] = combinedCookieString;
request.headers['cookie'] = cookieString;
}
}
@@ -378,8 +323,7 @@ const runSingleRequest = async function (
error: err?.message || err?.errors?.map(e => e?.message)?.at(0) || err?.code || 'Request Failed!',
assertionResults: [],
testResults: [],
nextRequestName: nextRequestName,
shouldStopRunnerExecution
nextRequestName: nextRequestName
};
}
}
@@ -419,16 +363,11 @@ const runSingleRequest = async function (
collectionPath,
null,
processEnvVars,
scriptingConfig,
runSingleRequestByPathname
scriptingConfig
);
if (result?.nextRequestName !== undefined) {
nextRequestName = result.nextRequestName;
}
if (result?.stopExecution) {
shouldStopRunnerExecution = true;
}
}
// run assertions
@@ -469,18 +408,13 @@ const runSingleRequest = async function (
collectionPath,
null,
processEnvVars,
scriptingConfig,
runSingleRequestByPathname
scriptingConfig
);
testResults = get(result, 'results', []);
if (result?.nextRequestName !== undefined) {
nextRequestName = result.nextRequestName;
}
if (result?.stopExecution) {
shouldStopRunnerExecution = true;
}
}
if (testResults?.length) {
@@ -513,8 +447,7 @@ const runSingleRequest = async function (
error: null,
assertionResults,
testResults,
nextRequestName: nextRequestName,
shouldStopRunnerExecution
nextRequestName: nextRequestName
};
} catch (err) {
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));

View File

@@ -204,6 +204,5 @@ module.exports = {
mergeHeaders,
mergeVars,
mergeScripts,
findItemInCollection,
getTreePathFromCollectionToItem
}

View File

@@ -9,7 +9,6 @@
"scripts": {
"clean": "rimraf dist",
"dev": "electron .",
"debug": "electron . --inspect=9229",
"dist:mac": "electron-builder --mac --config electron-builder-config.js",
"dist:win": "electron-builder --win --config electron-builder-config.js",
"dist:linux": "electron-builder --linux AppImage --config electron-builder-config.js",
@@ -34,7 +33,7 @@
"@usebruno/vm2": "^3.9.13",
"about-window": "^1.15.2",
"aws4-axios": "^3.3.0",
"axios": "1.7.7",
"axios": "1.7.5",
"axios-ntlm": "^1.4.2",
"chai": "^4.3.7",
"chokidar": "^3.5.3",

View File

@@ -1,9 +1,8 @@
const { sizeInMB } = require("../../utils/filesystem");
const WorkerQueue = require("../../workers");
const path = require("path");
const getSize = (data) => {
return sizeInMB(typeof data === 'string' ? Buffer.byteLength(data, 'utf8') : Buffer.byteLength(JSON.stringify(data), 'utf8'));
return typeof data === 'string' ? Buffer.byteLength(data, 'utf8') : Buffer.byteLength(JSON.stringify(data), 'utf8');
}
/**
@@ -13,13 +12,7 @@ const getSize = (data) => {
* This helps with parsing performance.
*/
const LANES = [{
maxSize: 0.005
},{
maxSize: 0.1
},{
maxSize: 1
},{
maxSize: 10
},{
maxSize: 100
}];

View File

@@ -20,6 +20,7 @@ const {
normalizeWslPath,
normalizeAndResolvePath,
safeToRename,
sanitizeCollectionName,
isWindowsOS,
isValidFilename,
hasSubDirectories,
@@ -61,11 +62,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
});
// browse directory for file
ipcMain.handle('renderer:browse-files', async (_, filters, properties) => {
ipcMain.handle('renderer:browse-files', async (event, pathname, request, filters) => {
try {
return await browseFiles(mainWindow, filters, properties);
const filePaths = await browseFiles(mainWindow, filters);
return filePaths;
} catch (error) {
throw error;
return Promise.reject(error);
}
});
@@ -75,6 +78,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
async (event, collectionName, collectionFolderName, collectionLocation) => {
try {
collectionFolderName = sanitizeDirectoryName(collectionFolderName);
collectionName = sanitizeCollectionName(collectionName);
const dirPath = path.join(collectionLocation, collectionFolderName);
if (fs.existsSync(dirPath)) {
const files = fs.readdirSync(dirPath);
@@ -116,7 +120,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle(
'renderer:clone-collection',
async (event, collectionName, collectionFolderName, collectionLocation, previousPath) => {
collectionFolderName = sanitizeDirectoryName(collectionFolderName);
collectionFolderName = sanitizeCollectionName(collectionFolderName);
const dirPath = path.join(collectionLocation, collectionFolderName);
if (fs.existsSync(dirPath)) {
throw new Error(`collection: ${dirPath} already exists`);
@@ -137,7 +141,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// Change new name of collection
let brunoConfig = JSON.parse(content);
brunoConfig.name = collectionName;
const cont = await stringifyJson(brunoConfig);
const cont = await stringifyJson(json);
// write the bruno.json to new dir
await writeFile(path.join(dirPath, 'bruno.json'), cont);
@@ -166,6 +170,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// rename collection
ipcMain.handle('renderer:rename-collection', async (event, newName, collectionPathname) => {
try {
newName = sanitizeCollectionName(newName);
const brunoJsonFilePath = path.join(collectionPathname, 'bruno.json');
const content = fs.readFileSync(brunoJsonFilePath, 'utf8');
const json = JSON.parse(content);
@@ -516,13 +521,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
ipcMain.handle('renderer:update-collection-paths', async (_, collectionPaths) => {
lastOpenedCollections.update(collectionPaths);
})
ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => {
try {
let collectionName = sanitizeDirectoryName(collection.name);
let collectionName = sanitizeCollectionName(collection.name);
let collectionPath = path.join(collectionLocation, collectionName);
if (fs.existsSync(collectionPath)) {
@@ -908,18 +909,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
watcher.addWatcher(mainWindow, collectionPathname, collectionUid, brunoConfig, false, shouldLoadCollectionAsync);
});
ipcMain.handle('renderer:show-in-folder', async (event, filePath) => {
try {
if (!filePath) {
throw new Error('File path is required');
}
shell.showItemInFolder(filePath);
} catch (error) {
console.error('Error in show-in-folder: ', error);
throw error;
}
});
};
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {

View File

@@ -28,7 +28,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, writeFile } = require('../../utils/filesystem');
const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem');
const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies');
const {
resolveOAuth2AuthorizationCodeAccessToken,
@@ -274,6 +274,7 @@ const configureRequest = async (
});
}
let axiosInstance = makeAxiosInstance();
if (request.ntlmConfig) {
@@ -332,30 +333,7 @@ const configureRequest = async (
if (preferencesUtil.shouldSendCookies()) {
const cookieString = getCookieStringForUrl(request.url);
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
const existingCookieHeaderName = Object.keys(request.headers).find(
name => name.toLowerCase() === 'cookie'
);
const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : '';
// Helper function to parse cookies into an object
const parseCookies = (str) => str.split(';').reduce((cookies, cookie) => {
const [name, ...rest] = cookie.split('=');
if (name && name.trim()) {
cookies[name.trim()] = rest.join('=').trim();
}
return cookies;
}, {});
const mergedCookies = {
...parseCookies(existingCookieString),
...parseCookies(cookieString),
};
const combinedCookieString = Object.entries(mergedCookies)
.map(([name, value]) => `${name}=${value}`)
.join('; ');
request.headers[existingCookieHeaderName || 'Cookie'] = combinedCookieString;
request.headers['cookie'] = cookieString;
}
}
@@ -425,7 +403,7 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
envVars,
collectionPath,
collection,
collectionRoot,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -459,8 +437,6 @@ const registerNetworkIpc = (mainWindow) => {
mainWindow.webContents.send('main:global-environment-variables-update', {
globalEnvironmentVariables: scriptResult.globalEnvironmentVariables
});
collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
}
// interpolate variables inside request
@@ -494,7 +470,7 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
envVars,
collectionPath,
collection,
collectionRoot,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -531,8 +507,6 @@ const registerNetworkIpc = (mainWindow) => {
if (result?.error) {
mainWindow.webContents.send('main:display-error', result.error);
}
collection.globalEnvironmentVariables = result.globalEnvironmentVariables;
}
// run post-response script
@@ -563,13 +537,11 @@ const registerNetworkIpc = (mainWindow) => {
mainWindow.webContents.send('main:global-environment-variables-update', {
globalEnvironmentVariables: scriptResult.globalEnvironmentVariables
});
collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
}
return scriptResult;
};
const runRequest = async ({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground = false }) => {
const runRequest = async ({ item, collection, environment, runtimeVariables, runInBackground = false }) => {
const collectionUid = collection.uid;
const collectionPath = collection.pathname;
const cancelTokenUid = uuid();
@@ -581,9 +553,9 @@ const registerNetworkIpc = (mainWindow) => {
if (itemPathname && !itemPathname?.endsWith('.bru')) {
itemPathname = `${itemPathname}.bru`;
}
const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname));
const _item = findItemInCollectionByPathname(collection, itemPathname);
if(_item) {
const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });
const res = await runRequest({ item: _item, collection, environment, runtimeVariables, runInBackground: true });
resolve(res);
}
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
@@ -598,23 +570,26 @@ const registerNetworkIpc = (mainWindow) => {
cancelTokenUid
});
const abortController = new AbortController();
const request = await prepareRequest(item, collection, abortController);
const collectionRoot = get(collection, 'root', {});
const request = prepareRequest(item, collection);
request.__bruno__executionMode = 'standalone';
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = getJsSandboxRuntime(collection);
try {
request.signal = abortController.signal;
saveCancelToken(cancelTokenUid, abortController);
const controller = new AbortController();
request.signal = controller.signal;
saveCancelToken(cancelTokenUid, controller);
await runPreRequest(
request,
requestUid,
envVars,
collectionPath,
collection,
collectionRoot,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -637,7 +612,7 @@ const registerNetworkIpc = (mainWindow) => {
url: request.url,
method: request.method,
headers: request.headers,
data: request.mode == 'file'? "<request body redacted>": safeParseJSON(safeStringifyJSON(request.data)) ,
data: safeParseJSON(safeStringifyJSON(request.data)),
timestamp: Date.now()
},
collectionUid,
@@ -699,7 +674,7 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
envVars,
collectionPath,
collection,
collectionRoot,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -783,10 +758,7 @@ const registerNetworkIpc = (mainWindow) => {
// handler for sending http request
ipcMain.handle('send-http-request', async (event, item, collection, environment, runtimeVariables) => {
const collectionUid = collection.uid;
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
return await runRequest({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: false });
return await runRequest({ item, collection, environment, runtimeVariables });
});
ipcMain.handle('send-collection-oauth2-request', async (event, collection, environment, runtimeVariables) => {
@@ -810,7 +782,7 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
envVars,
collectionPath,
collection,
collectionRoot,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -846,7 +818,7 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
envVars,
collectionPath,
collection,
collectionRoot,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -916,7 +888,7 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
envVars,
collectionPath,
collection,
collectionRoot,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -940,7 +912,7 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
envVars,
collectionPath,
collection,
collectionRoot,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -977,8 +949,7 @@ const registerNetworkIpc = (mainWindow) => {
const brunoConfig = getBrunoConfig(collectionUid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = getJsSandboxRuntime(collection);
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
const collectionRoot = get(collection, 'root', {});
let stopRunnerExecution = false;
const abortController = new AbortController();
@@ -990,9 +961,9 @@ const registerNetworkIpc = (mainWindow) => {
if (itemPathname && !itemPathname?.endsWith('.bru')) {
itemPathname = `${itemPathname}.bru`;
}
const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname));
const _item = findItemInCollectionByPathname(collection, itemPathname);
if(_item) {
const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });
const res = await runRequest({ item: _item, collection, environment, runtimeVariables, runInBackground: true });
resolve(res);
}
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
@@ -1012,6 +983,7 @@ const registerNetworkIpc = (mainWindow) => {
});
try {
const envVars = getEnvVars(environment);
let folderRequests = [];
if (recursive) {
@@ -1059,10 +1031,11 @@ const registerNetworkIpc = (mainWindow) => {
...eventData
});
const request = await prepareRequest(item, collection, abortController);
const request = prepareRequest(item, collection);
request.__bruno__executionMode = 'runner';
const requestUid = uuid();
const processEnvVars = getProcessEnvVars(collectionUid);
try {
const preRequestScriptResult = await runPreRequest(
@@ -1070,7 +1043,7 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
envVars,
collectionPath,
collection,
collectionRoot,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -1208,7 +1181,7 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
envVars,
collectionPath,
collection,
collectionRoot,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -1394,7 +1367,7 @@ const registerNetworkIpc = (mainWindow) => {
if (encoding === 'utf-8') {
await writeFile(filePath, data);
} else {
await writeFile(filePath, data, true);
await writeBinaryFile(filePath, data);
}
}
} catch (error) {

View File

@@ -65,11 +65,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
const contentType = getContentType(request.headers);
/*
We explicitly avoid interpolating buffer values because the file content is read as a buffer object in raw body mode.
Even if the selected file's content type is JSON, this prevents the buffer object from being interpolated.
*/
if (contentType.includes('json') && !Buffer.isBuffer(request.data)) {
if (contentType.includes('json')) {
if (typeof request.data === 'string') {
if (request.data.length) {
request.data = _interpolate(request.data);

View File

@@ -1,10 +1,8 @@
const { get, each, filter, find } = require('lodash');
const { get, each, filter } = require('lodash');
const decomment = require('decomment');
const crypto = require('node:crypto');
const fs = require('node:fs/promises');
const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars } = require('../../utils/collection');
const { buildFormUrlEncodedPayload, createFormData } = require('../../utils/form-data');
const path = require('node:path');
const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
@@ -176,10 +174,10 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
return axiosRequest;
};
const prepareRequest = async (item, collection = {}, abortController) => {
const prepareRequest = (item, collection) => {
const request = item.draft ? item.draft.request : item.request;
const collectionRoot = get(collection, 'root', {});
const collectionPath = collection?.pathname;
const collectionPath = collection.pathname;
const headers = {};
let contentTypeDefined = false;
let url = request.url;
@@ -191,7 +189,7 @@ const prepareRequest = async (item, collection = {}, abortController) => {
}
});
const scriptFlow = collection?.brunoConfig?.scripts?.flow ?? 'sandwich';
const scriptFlow = collection.brunoConfig?.scripts?.flow ?? 'sandwich';
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
if (requestTreePath && requestTreePath.length > 0) {
mergeHeaders(collection, request, requestTreePath);
@@ -253,31 +251,6 @@ const prepareRequest = async (item, collection = {}, abortController) => {
axiosRequest.data = request.body.sparql;
}
if (request.body.mode === 'file') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/octet-stream'; // Default headers for binary file uploads
}
const bodyFile = find(request.body.file, (param) => param.selected);
if (bodyFile) {
let { filePath, contentType } = bodyFile;
axiosRequest.headers['content-type'] = contentType;
if (filePath) {
if (!path.isAbsolute(filePath)) {
filePath = path.join(collectionPath, filePath);
}
try {
const fileContent = await fs.readFile(filePath);
axiosRequest.data = fileContent;
} catch (error) {
console.error('Error reading file:', error);
}
}
}
}
if (request.body.mode === 'formUrlEncoded') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';

View File

@@ -16,20 +16,18 @@ class LastOpenedCollections {
}
add(collectionPath) {
const collections = this.getAll();
const collections = this.store.get('lastOpenedCollections') || [];
if (isDirectory(collectionPath) && !collections.includes(collectionPath)) {
collections.push(collectionPath);
this.store.set('lastOpenedCollections', collections);
if (isDirectory(collectionPath)) {
if (!collections.includes(collectionPath)) {
collections.push(collectionPath);
this.store.set('lastOpenedCollections', collections);
}
}
}
update(collectionPaths) {
this.store.set('lastOpenedCollections', collectionPaths);
}
remove(collectionPath) {
let collections = this.getAll();
let collections = this.store.get('lastOpenedCollections') || [];
if (collections.includes(collectionPath)) {
collections = _.filter(collections, (c) => c !== collectionPath);
@@ -38,7 +36,7 @@ class LastOpenedCollections {
}
removeAll() {
this.store.set('lastOpenedCollections', []);
return this.store.set('lastOpenedCollections', []);
}
}

View File

@@ -11,7 +11,7 @@ const mergeHeaders = (collection, request, requestTreePath) => {
let collectionHeaders = get(collection, 'root.request.headers', []);
collectionHeaders.forEach((header) => {
if (header.enabled) {
headers.set(header.name?.toLowerCase?.(), header.value);
headers.set(header.name, header.value);
if (header?.name?.toLowerCase() === 'content-type') {
contentTypeDefined = true;
}
@@ -23,14 +23,14 @@ const mergeHeaders = (collection, request, requestTreePath) => {
let _headers = get(i, 'root.request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
headers.set(header.name?.toLowerCase?.(), header.value);
headers.set(header.name, header.value);
}
});
} else {
const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
headers.set(header.name?.toLowerCase?.(), header.value);
headers.set(header.name, header.value);
}
});
}
@@ -240,7 +240,6 @@ const hydrateRequestWithUuid = (request, pathname) => {
const assertions = get(request, 'request.assertions', []);
const bodyFormUrlEncoded = get(request, 'request.body.formUrlEncoded', []);
const bodyMultipartForm = get(request, 'request.body.multipartForm', []);
const file = get(request, 'request.body.file', []);
params.forEach((param) => (param.uid = uuid()));
headers.forEach((header) => (header.uid = uuid()));
@@ -249,7 +248,6 @@ const hydrateRequestWithUuid = (request, pathname) => {
assertions.forEach((assertion) => (assertion.uid = uuid()));
bodyFormUrlEncoded.forEach((param) => (param.uid = uuid()));
bodyMultipartForm.forEach((param) => (param.uid = uuid()));
file.forEach((param) => (param.uid = uuid()));
return request;
};

View File

@@ -68,13 +68,20 @@ function normalizeWslPath(pathname) {
return pathname.replace(/^\/wsl.localhost/, '\\\\wsl.localhost').replace(/\//g, '\\');
}
const writeFile = async (pathname, content, isBinary = false) => {
const writeFile = async (pathname, content) => {
try {
await fs.writeFile(pathname, content, {
encoding: !isBinary ? "utf-8" : null
fs.writeFileSync(pathname, content, {
encoding: 'utf8'
});
} catch (err) {
console.error(`Error writing file at ${pathname}:`, err);
return Promise.reject(err);
}
};
const writeBinaryFile = async (pathname, content) => {
try {
fs.writeFileSync(pathname, content);
} catch (err) {
return Promise.reject(err);
}
};
@@ -114,9 +121,9 @@ const browseDirectory = async (win) => {
return isDirectory(resolvedPath) ? resolvedPath : false;
};
const browseFiles = async (win, filters = [], properties = []) => {
const browseFiles = async (win, filters) => {
const { filePaths } = await dialog.showOpenDialog(win, {
properties: ['openFile', ...properties],
properties: ['openFile', 'multiSelections'],
filters
});
@@ -154,6 +161,10 @@ const searchForBruFiles = (dir) => {
return searchForFiles(dir, '.bru');
};
const sanitizeCollectionName = (name) => {
return name.trim();
}
const sanitizeDirectoryName = (name) => {
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-').trim();
};
@@ -254,6 +265,7 @@ module.exports = {
isWSLPath,
normalizeWslPath,
writeFile,
writeBinaryFile,
hasJsonExtension,
hasBruExtension,
createDirectory,
@@ -263,6 +275,7 @@ module.exports = {
searchForFiles,
searchForBruFiles,
sanitizeDirectoryName,
sanitizeCollectionName,
isWindowsOS,
safeToRename,
isValidFilename,

View File

@@ -7,17 +7,15 @@ 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 result = await prepareRequest({ request: { body }, collection: { pathname: '' } });
const expected = '{\n"test": "{{someVar}}" \n}';
const result = prepareRequest({ request: { body } }, {});
expect(result.data).toEqual(expected);
});
it('If request body is not valid JSON', async () => {
const body = { mode: 'json', json: '{\n"test": {{someVar}} // comment\n}' };
const expected = '{\n"test": {{someVar}} \n}';
const result = await prepareRequest({ request: { body }, collection: { pathname: '' } });
const result = prepareRequest({ request: { body } }, {});
expect(result.data).toEqual(expected);
});

View File

@@ -21,11 +21,10 @@
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"atob": "^2.1.2",
"axios": "1.7.7",
"axios": "1.7.5",
"btoa": "^1.2.1",
"chai": "^4.3.7",
"chai-string": "^1.5.0",
"cheerio": "^1.0.0",
"crypto-js": "^4.1.1",
"json-query": "^2.2.2",
"lodash": "^4.17.21",
@@ -35,8 +34,7 @@
"node-vault": "^0.10.2",
"path": "^0.12.7",
"quickjs-emscripten": "^0.29.2",
"uuid": "^9.0.0",
"xml2js": "^0.6.2"
"uuid": "^9.0.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.2",

View File

@@ -1,5 +1,3 @@
const { get } = require('@usebruno/query');
class BrunoResponse {
constructor(res) {
this.res = res;
@@ -8,13 +6,6 @@ class BrunoResponse {
this.headers = res ? res.headers : null;
this.body = res ? res.data : null;
this.responseTime = res ? res.responseTime : null;
// Make the instance callable
const callable = (...args) => get(this.body, ...args);
Object.setPrototypeOf(callable, this.constructor.prototype);
Object.assign(callable, this);
return callable;
}
getStatus() {

View File

@@ -28,8 +28,6 @@ const fetch = require('node-fetch');
const chai = require('chai');
const CryptoJS = require('crypto-js');
const NodeVault = require('node-vault');
const xml2js = require('xml2js');
const cheerio = require('cheerio');
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
class ScriptRuntime {
@@ -147,8 +145,6 @@ class ScriptRuntime {
chai,
'node-fetch': fetch,
'crypto-js': CryptoJS,
'xml2js': xml2js,
cheerio,
...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined,
'node-vault': NodeVault

View File

@@ -30,8 +30,6 @@ const axios = require('axios');
const fetch = require('node-fetch');
const CryptoJS = require('crypto-js');
const NodeVault = require('node-vault');
const xml2js = require('xml2js');
const cheerio = require('cheerio');
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
const getResultsSummary = (results) => {
@@ -207,8 +205,6 @@ class TestRuntime {
chai,
'node-fetch': fetch,
'crypto-js': CryptoJS,
'xml2js': xml2js,
cheerio,
...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined,
'node-vault': NodeVault

View File

@@ -25,7 +25,7 @@ const grammar = ohm.grammar(`Bru {
BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)*
auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
bodyforms = bodyformurlencoded | bodymultipart | bodyfile
bodyforms = bodyformurlencoded | bodymultipart
params = paramspath | paramsquery
nl = "\\r"? "\\n"
@@ -102,8 +102,7 @@ const grammar = ohm.grammar(`Bru {
bodyformurlencoded = "body:form-urlencoded" dictionary
bodymultipart = "body:multipart-form" dictionary
bodyfile = "body:file" dictionary
script = scriptreq | scriptres
scriptreq = "script:pre-request" st* "{" nl* textblock tagend
scriptres = "script:post-response" st* "{" nl* textblock tagend
@@ -174,19 +173,6 @@ const multipartExtractContentType = (pair) => {
}
};
const fileExtractContentType = (pair) => {
if (_.isString(pair.value)) {
const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/);
if (match && match.length > 2) {
pair.value = match[1].trim();
pair.contentType = match[2].trim();
} else {
pair.contentType = '';
}
}
};
const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => {
const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
@@ -204,27 +190,6 @@ const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) =
});
};
const mapPairListToKeyValPairsFile = (pairList = [], parseEnabled = true) => {
const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
return pairs.map((pair) => {
fileExtractContentType(pair);
if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) {
let filePath = pair.value.replace(/^@file\(/, '').replace(/\)$/, '');
pair.filePath = filePath;
pair.selected = pair.enabled
// Remove pair.value as it only contains the file path reference
delete pair.value;
// Remove pair.name as it is auto-generated (e.g., file1, file2, file3, etc.)
delete pair.name;
delete pair.enabled;
}
return pair;
});
};
const concatArrays = (objValue, srcValue) => {
if (_.isArray(objValue) && _.isArray(srcValue)) {
return objValue.concat(srcValue);
@@ -609,13 +574,6 @@ const sem = grammar.createSemantics().addAttribute('ast', {
}
};
},
bodyfile(_1, dictionary) {
return {
body: {
file: mapPairListToKeyValPairsFile(dictionary.ast)
}
};
},
body(_1, _2, _3, _4, textblock, _5) {
return {
http: {
@@ -750,4 +708,3 @@ const parser = (input) => {
};
module.exports = parser;

View File

@@ -2,8 +2,8 @@ const _ = require('lodash');
const { indentString } = require('../../v1/src/utils');
const enabled = (items = [], key = "enabled") => items.filter((item) => item[key]);
const disabled = (items = [], key = "enabled") => items.filter((item) => !item[key]);
const enabled = (items = []) => items.filter((item) => item.enabled);
const disabled = (items = []) => items.filter((item) => !item.enabled);
// remove the last line if two new lines are found
const stripLastLine = (text) => {
@@ -313,30 +313,6 @@ ${indentString(body.sparql)}
bru += '\n}\n\n';
}
if (body && body.file && body.file.length) {
bru += `body:file {`;
const files = enabled(body.file, "selected").concat(disabled(body.file, "selected"));
if (files.length) {
bru += `\n${indentString(
files
.map((item) => {
const selected = item.selected ? '' : '~';
const contentType =
item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';
const filePath = item.filePath || '';
const value = `@file(${filePath})`;
const itemName = "file";
return `${selected}${itemName}: ${value}${contentType}`;
})
.join('\n')
)}`;
}
bru += '\n}\n\n';
}
if (body && body.graphql && body.graphql.query) {
bru += `body:graphql {\n`;
bru += `${indentString(body.graphql.query)}`;

View File

@@ -102,12 +102,6 @@ body:multipart-form {
~message: hello
}
body:file {
file: @file(path/to/file.json) @contentType(application/json)
file: @file(path/to/file.json) @contentType(application/json)
~file: @file(path/to/file2.json) @contentType(application/json)
}
body:graphql {
{
launchesPast {

View File

@@ -137,23 +137,6 @@
"enabled": false,
"type": "text"
}
],
"file" : [
{
"filePath": "path/to/file.json",
"contentType": "application/json",
"selected": true
},
{
"filePath": "path/to/file.json",
"contentType": "application/json",
"selected": true
},
{
"filePath": "path/to/file2.json",
"contentType": "application/json",
"selected": false
}
]
},
"vars": {

View File

@@ -74,19 +74,9 @@ const multipartFormSchema = Yup.object({
.noUnknown(true)
.strict();
const fileSchema = Yup.object({
uid: uidSchema,
filePath: Yup.string().nullable(),
contentType: Yup.string().nullable(),
selected: Yup.boolean()
})
.noUnknown(true)
.strict();
const requestBodySchema = Yup.object({
mode: Yup.string()
.oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql', 'file'])
.oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql'])
.required('mode is required'),
json: Yup.string().nullable(),
text: Yup.string().nullable(),
@@ -94,8 +84,7 @@ const requestBodySchema = Yup.object({
sparql: Yup.string().nullable(),
formUrlEncoded: Yup.array().of(keyValueSchema).nullable(),
multipartForm: Yup.array().of(multipartFormSchema).nullable(),
graphql: graphqlBodySchema.nullable(),
file: Yup.array().of(fileSchema).nullable()
graphql: graphqlBodySchema.nullable()
})
.noUnknown(true)
.strict();

View File

@@ -1,15 +0,0 @@
meta {
name: echo binary
type: http
seq: 1
}
post {
url: {{echo-host}}
body: file
auth: none
}
body:file {
file: @file(bruno.png) @contentType(image/png)
}

View File

@@ -1,3 +0,0 @@
file.txt
hello, bruno

View File

@@ -9,7 +9,3 @@ get {
body: none
auth: none
}
script:pre-request {
bru.runner.stopExecution();
}

View File

@@ -1,60 +0,0 @@
meta {
name: runRequest-1
type: http
seq: 10
}
post {
url: {{echo-host}}
body: text
auth: none
}
body:text {
bruno
}
script:pre-request {
// reset values
bru.setVar('run-request-runtime-var', null);
bru.setEnvVar('run-request-env-var', null);
bru.setGlobalEnvVar('run-request-global-env-var', null);
// the above vars will be set in the below request
const resp = await bru.runRequest('scripting/api/bru/runRequest-2');
bru.setVar('run-request-resp', {
data: resp?.data,
statusText: resp?.statusText,
status: resp?.status
});
}
tests {
test("should get runtime var set in runRequest-2", function() {
const val = bru.getVar("run-request-runtime-var");
expect(val).to.equal("run-request-runtime-var-value");
});
test("should get env var set in runRequest-2", function() {
const val = bru.getEnvVar("run-request-env-var");
expect(val).to.equal("run-request-env-var-value");
});
test("should get global env var set in runRequest-2", function() {
const val = bru.getGlobalEnvVar("run-request-global-env-var");
const executionMode = req.getExecutionMode();
if (executionMode == 'runner') {
expect(val).to.equal("run-request-global-env-var-value");
}
});
test("should get response of runRequest-2", function() {
const val = bru.getVar('run-request-resp');
expect(JSON.stringify(val)).to.equal(JSON.stringify({
"data": "bruno",
"statusText": "OK",
"status": 200
}));
});
}

View File

@@ -1,21 +0,0 @@
meta {
name: runRequest-2
type: http
seq: 11
}
post {
url: {{echo-host}}
body: text
auth: none
}
body:text {
bruno
}
script:pre-request {
bru.setVar('run-request-runtime-var', 'run-request-runtime-var-value');
bru.setEnvVar('run-request-env-var', 'run-request-env-var-value');
bru.setGlobalEnvVar('run-request-global-env-var', 'run-request-global-env-var-value');
}

View File

@@ -1,96 +0,0 @@
meta {
name: runRequest
type: http
seq: 2
}
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": "bruno"
}
}
assert {
res.status: eq 200
}
script:pre-request {
bru.setVar("runRequest-ping-res-1", null);
bru.setVar("runRequest-ping-res-2", null);
bru.setVar("runRequest-ping-res-3", null);
let pingRes = await bru.runRequest('ping');
bru.setVar('runRequest-ping-res-1', {
data: pingRes?.data,
statusText: pingRes?.statusText,
status: pingRes?.status
});
}
script:post-response {
let pingRes = await bru.runRequest('ping');
bru.setVar('runRequest-ping-res-2', {
data: pingRes?.data,
statusText: pingRes?.statusText,
status: pingRes?.status
});
}
tests {
const pingRes = await bru.runRequest('ping');
bru.setVar('runRequest-ping-res-3', {
data: pingRes?.data,
statusText: pingRes?.statusText,
status: pingRes?.status
});
test("should run request and return valid response in pre-request script", function() {
const expectedPingRes = {
data: "pong",
statusText: "OK",
status: 200
};
const pingRes = bru.getVar('runRequest-ping-res-1');
expect(pingRes).to.eql(expectedPingRes);
});
test("should run request and return valid response in post-response script", function() {
const expectedPingRes = {
data: "pong",
statusText: "OK",
status: 200
};
const pingRes = bru.getVar('runRequest-ping-res-2');
expect(pingRes).to.eql(expectedPingRes);
});
test("should run request and return valid response in tests script", function() {
const expectedPingRes = {
data: "pong",
statusText: "OK",
status: 200
};
const pingRes = bru.getVar('runRequest-ping-res-3');
expect(pingRes).to.eql(expectedPingRes);
});
}

View File

@@ -1,19 +0,0 @@
meta {
name: 1
type: http
seq: 1
}
post {
url: https://echo.usebruno.com
body: none
auth: none
}
script:pre-request {
bru.setVar('bru-runner-req', 1);
}
script:post-response {
bru.setVar('bru.runner.skipRequest', true);
}

View File

@@ -1,19 +0,0 @@
meta {
name: 2
type: http
seq: 2
}
post {
url: https://echo.usebruno.com
body: none
auth: none
}
script:pre-request {
bru.runner.skipRequest();
}
script:post-response {
bru.setVar('bru.runner.skipRequest', false);
}

View File

@@ -1,11 +0,0 @@
meta {
name: 3
type: http
seq: 3
}
post {
url: https://echo.usebruno.com
body: none
auth: none
}

View File

@@ -1,42 +0,0 @@
meta {
name: cheerio
type: http
seq: 1
}
post {
url: https://echo.usebruno.com
body: text
auth: none
}
body:text {
<h2 class="title">Hello Bruno!</h2>
}
script:pre-request {
const cheerio = require('cheerio');
const $ = cheerio.load('<h2 class="title">Hello world</h2>');
$('h2.title').text('Hello there!');
$('h2').addClass('welcome');
bru.setVar("cheerio-test-html", $.html());
}
tests {
const cheerio = require('cheerio');
test("cheerio html - from scripts", function() {
const expected = '<html><head></head><body><h2 class="title welcome">Hello there!</h2></body></html>';
const html = bru.getVar('cheerio-test-html');
expect(html).to.eql(expected);
});
test("cheerio html - from tests", function() {
const expected = '<html><head></head><body><h2 class="title">Hello Bruno!</h2></body></html>';
const $ = cheerio.load(res.body);
expect($.html()).to.eql(expected);
});
}

View File

@@ -1,41 +0,0 @@
meta {
name: xml2js
type: http
seq: 1
}
get {
url: {{host}}/ping
body: none
auth: none
}
script:pre-request {
var parseString = require('xml2js').parseString;
var xml = "<root>Hello xml2js!</root>"
parseString(xml, function (err, result) {
bru.setVar("xml2js-test-result", result);
});
}
tests {
var parseString = require('xml2js').parseString;
test("xml2js parseString in scripts", function() {
const expected = {
root: 'Hello xml2js!'
};
const result = bru.getVar('xml2js-test-result');
expect(result).to.eql(expected);
});
test("xml2js parseString in tests", async function() {
var xml = "<root>Hello inside test!</root>"
const expected = {
root: 'Hello inside test!'
};
parseString(xml, function (err, result) {
expect(result).to.eql(expected);
});
});
}

View File

@@ -18,7 +18,7 @@
},
"homepage": "https://github.com/usebruno/bruno-testbench#readme",
"dependencies": {
"axios": "1.7.7",
"axios": "1.7.5",
"body-parser": "1.20.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",

View File

@@ -19,17 +19,6 @@ router.post('/xml-raw', (req, res) => {
return res.send(req.rawBody);
});
router.post('/bin', (req, res) => {
const rawBody = req.body;
if (!rawBody || rawBody.length === 0) {
return res.status(400).send('No data received');
}
res.set('Content-Type', req.headers['content-type'] || 'application/octet-stream');
res.send(rawBody);
});
router.get('/bom-json-test', (req, res) => {
const jsonData = {
message: 'Hello!',

View File

@@ -10,7 +10,6 @@ const multipartRouter = require('./multipart');
const app = new express();
const port = process.env.PORT || 8080;
app.use(express.raw({type: '*/*', limit: '100mb'}));
app.use(cors());
app.use(xmlParser());
app.use(bodyParser.text());