From dce14811854b40201b35eeb51b8cd17f54cf739d Mon Sep 17 00:00:00 2001 From: Nelu Platonov Date: Thu, 23 Nov 2023 23:38:43 +0100 Subject: [PATCH 001/140] fix(#521): Allow "context" as the name of a key/var in a JS expression --- packages/bruno-js/src/utils.js | 10 +++++---- packages/bruno-js/tests/utils.spec.js | 30 ++++++++++++++++++++------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/bruno-js/src/utils.js b/packages/bruno-js/src/utils.js index 7d6b2db30..92f3a2c08 100644 --- a/packages/bruno-js/src/utils.js +++ b/packages/bruno-js/src/utils.js @@ -18,8 +18,8 @@ const JS_KEYWORDS = ` * ```js * res.data.pets.map(pet => pet.name.toUpperCase()) * - * function(context) { - * const { res, pet } = context; + * function(_BrunoNewFunctionInnerContext) { + * const { res, pet } = _BrunoNewFunctionInnerContext; * return res.data.pets.map(pet => pet.name.toUpperCase()) * } * ``` @@ -45,9 +45,11 @@ const compileJsExpression = (expr) => { globals: globals.map((name) => ` ${name} = ${name} ?? globalThis.${name};`).join('') }; - const body = `let { ${code.vars} } = context; ${code.globals}; return ${expr}`; + // param name that is unlikely to show up as a var in an expression + const param = `_BrunoNewFunctionInnerContext`; + const body = `let { ${code.vars} } = ${param}; ${code.globals}; return ${expr}`; - return new Function('context', body); + return new Function(param, body); }; const internalExpressionCache = new Map(); diff --git a/packages/bruno-js/tests/utils.spec.js b/packages/bruno-js/tests/utils.spec.js index 6ac687f07..6a454fde7 100644 --- a/packages/bruno-js/tests/utils.spec.js +++ b/packages/bruno-js/tests/utils.spec.js @@ -5,7 +5,9 @@ describe('utils', () => { describe('expression evaluation', () => { const context = { res: { - data: { pets: ['bruno', 'max'] } + data: { pets: ['bruno', 'max'] }, + context: 'testContext', + _BrunoNewFunctionInnerContext: 0 } }; @@ -45,32 +47,32 @@ describe('utils', () => { it('should identify top level variables', () => { const expr = 'res.data.pets[0].toUpperCase()'; evaluateJsExpression(expr, context); - expect(cache.get(expr).toString()).toContain('let { res } = context;'); + expect(cache.get(expr).toString()).toContain('let { res } = _BrunoNewFunctionInnerContext;'); }); it('should not duplicate variables', () => { const expr = 'res.data.pets[0] + res.data.pets[1]'; evaluateJsExpression(expr, context); - expect(cache.get(expr).toString()).toContain('let { res } = context;'); + expect(cache.get(expr).toString()).toContain('let { res } = _BrunoNewFunctionInnerContext;'); }); it('should exclude js keywords like true false from vars', () => { const expr = 'res.data.pets.length > 0 ? true : false'; evaluateJsExpression(expr, context); - expect(cache.get(expr).toString()).toContain('let { res } = context;'); + expect(cache.get(expr).toString()).toContain('let { res } = _BrunoNewFunctionInnerContext;'); }); it('should exclude numbers from vars', () => { const expr = 'res.data.pets.length + 10'; evaluateJsExpression(expr, context); - expect(cache.get(expr).toString()).toContain('let { res } = context;'); + expect(cache.get(expr).toString()).toContain('let { res } = _BrunoNewFunctionInnerContext;'); }); it('should pick variables from complex expressions', () => { const expr = 'res.data.pets.map(pet => pet.length)'; const result = evaluateJsExpression(expr, context); expect(result).toEqual([5, 3]); - expect(cache.get(expr).toString()).toContain('let { res, pet } = context;'); + expect(cache.get(expr).toString()).toContain('let { res, pet } = _BrunoNewFunctionInnerContext;'); }); it('should be ok picking extra vars from strings', () => { @@ -78,7 +80,7 @@ describe('utils', () => { const result = evaluateJsExpression(expr, context); expect(result).toBe('hello bruno'); // extra var hello is harmless - expect(cache.get(expr).toString()).toContain('let { hello, res } = context;'); + expect(cache.get(expr).toString()).toContain('let { hello, res } = _BrunoNewFunctionInnerContext;'); }); it('should evaluate expressions referencing globals', () => { @@ -112,6 +114,20 @@ describe('utils', () => { expect(result).toBe(startTime); }); + + it('should allow "context" as a var name', () => { + const expr = 'res["context"].toUpperCase()'; + evaluateJsExpression(expr, context); + expect(cache.get(expr).toString()).toContain('let { res, context } = _BrunoNewFunctionInnerContext;'); + }); + + it('should throw an error when we use "_BrunoNewFunctionInnerContext" as a var name', () => { + const expr = 'res["_BrunoNewFunctionInnerContext"].toUpperCase()'; + expect(() => evaluateJsExpression(expr, context)).toThrow(SyntaxError); + expect(() => evaluateJsExpression(expr, context)).toThrow( + "Identifier '_BrunoNewFunctionInnerContext' has already been declared" + ); + }); }); describe('response parser', () => { From 39f60daca70708127e1019e87fd85f54ac62f625 Mon Sep 17 00:00:00 2001 From: busy-panda Date: Thu, 18 Apr 2024 15:43:09 +0200 Subject: [PATCH 002/140] feature: Multi-part requests: user should be able to set content-type for each part in a multi-part request. #1602 --- .../MultipartFormParams/StyledWrapper.js | 2 +- .../RequestPane/MultipartFormParams/index.js | 25 ++++++++ .../ReduxStore/slices/collections/index.js | 2 + .../bruno-cli/src/runner/prepare-request.js | 51 +++++++++++---- .../src/ipc/network/prepare-request.js | 10 ++- packages/bruno-lang/v2/src/bruToJson.js | 17 ++++- packages/bruno-lang/v2/src/jsonToBru.js | 6 +- .../bruno-lang/v2/tests/fixtures/request.json | 3 + .../bruno-schema/src/collections/index.js | 1 + .../multipart/mixed-content-types.bru | 39 ++++++++++++ .../collection/multipart/small.png | Bin 0 -> 132 bytes packages/bruno-tests/src/index.js | 10 ++- .../src/multipart/form-data-parser.js | 58 ++++++++++++++++++ packages/bruno-tests/src/multipart/index.js | 10 +++ 14 files changed, 208 insertions(+), 26 deletions(-) create mode 100644 packages/bruno-tests/collection/multipart/mixed-content-types.bru create mode 100644 packages/bruno-tests/collection/multipart/small.png create mode 100644 packages/bruno-tests/src/multipart/form-data-parser.js create mode 100644 packages/bruno-tests/src/multipart/index.js diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js index f04a30be0..80a146a5c 100644 --- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js @@ -24,7 +24,7 @@ const Wrapper = styled.div` width: 30%; } - &:nth-child(3) { + &:nth-child(4) { width: 70px; } } diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js index 1f1c9977e..794e35add 100644 --- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js @@ -52,6 +52,10 @@ const MultipartFormParams = ({ item, collection }) => { param.value = e.target.value; break; } + case 'contentType': { + param.contentType = e.target.value; + break; + } case 'enabled': { param.enabled = e.target.checked; break; @@ -83,6 +87,7 @@ const MultipartFormParams = ({ item, collection }) => { Key Value + Content-Type @@ -142,6 +147,26 @@ const MultipartFormParams = ({ item, collection }) => { /> )} + + + handleParamChange( + { + target: { + value: newValue + } + }, + param, + 'contentType' + ) + } + onRun={handleRun} + collection={collection} + /> +
{ + // make axios work in node using form data + // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427 + const form = new FormData(); + datas.forEach((item) => { + const value = item.value; + const name = item.name; + let options = {}; + if (item.contentType) { + options.contentType = item.contentType; + } + if (item.type === 'file') { + const filePaths = value || []; + filePaths.forEach((filePath) => { + let trimmedFilePath = filePath.trim(); + + if (!path.isAbsolute(trimmedFilePath)) { + trimmedFilePath = path.join(collectionPath, trimmedFilePath); + } + options.filename = path.basename(trimmedFilePath); + form.append(name, fs.createReadStream(trimmedFilePath), options); + }); + } else { + form.append(name, value, options); + } + }); + return form; +}; const prepareRequest = (request, collectionRoot) => { const headers = {}; @@ -124,17 +155,11 @@ const prepareRequest = (request, collectionRoot) => { } if (request.body.mode === 'multipartForm') { - const params = {}; const enabledParams = filter(request.body.multipartForm, (p) => p.enabled); - each(enabledParams, (p) => { - if (p.type === 'file') { - params[p.name] = p.value.map((path) => fs.createReadStream(path)); - } else { - params[p.name] = p.value; - } - }); - axiosRequest.headers['content-type'] = 'multipart/form-data'; - axiosRequest.data = params; + const collectionPath = process.cwd(); + const form = parseFormData(enabledParams, collectionPath); + extend(axiosRequest.headers, form.getHeaders()); + axiosRequest.data = form; } if (request.body.mode === 'graphql') { diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 37196589a..249b1754f 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -12,6 +12,10 @@ const parseFormData = (datas, collectionPath) => { datas.forEach((item) => { const value = item.value; const name = item.name; + let options = {}; + if (item.contentType) { + options.contentType = item.contentType; + } if (item.type === 'file') { const filePaths = value || []; filePaths.forEach((filePath) => { @@ -20,11 +24,11 @@ const parseFormData = (datas, collectionPath) => { if (!path.isAbsolute(trimmedFilePath)) { trimmedFilePath = path.join(collectionPath, trimmedFilePath); } - - form.append(name, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); + options.filename = path.basename(trimmedFilePath); + form.append(name, fs.createReadStream(trimmedFilePath), options); }); } else { - form.append(name, value); + form.append(name, value, options); } }); return form; diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 6f12a6ce5..32ac3a467 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -45,7 +45,7 @@ const grammar = ohm.grammar(`Bru { pair = st* key st* ":" st* value st* key = keychar* value = multilinetextblock | valuechar* - + // Dictionary for Assert Block assertdictionary = st* "{" assertpairlist? tagend assertpairlist = optionalnl* assertpair (~tagend stnl* assertpair)* (~tagend space)* @@ -133,16 +133,31 @@ const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => { }); }; +const multipartExtractContentType = (pair) => { + if (_.isString(pair.value)) { + const match = pair.value.match(/^(.*?)\s*\(Content-Type=(.*?)\)\s*$/); + if (match != null && match.length > 2) { + pair.value = match[1]; + pair.contentType = match[2]; + } else { + pair.contentType = ''; + } + } +}; + const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => { const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); return pairs.map((pair) => { pair.type = 'text'; + multipartExtractContentType(pair); + if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) { let filestr = pair.value.replace(/^@file\(/, '').replace(/\)$/, ''); pair.type = 'file'; pair.value = filestr.split('|'); } + return pair; }); }; diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 3357e5d09..658d7cbd2 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -247,16 +247,18 @@ ${indentString(body.sparql)} multipartForms .map((item) => { const enabled = item.enabled ? '' : '~'; + const contentType = + item.contentType && item.contentType !== '' ? ' (Content-Type=' + item.contentType + ')' : ''; if (item.type === 'text') { - return `${enabled}${item.name}: ${item.value}`; + return `${enabled}${item.name}: ${item.value}${contentType}`; } if (item.type === 'file') { let filepaths = item.value || []; let filestr = filepaths.join('|'); const value = `@file(${filestr})`; - return `${enabled}${item.name}: ${value}`; + return `${enabled}${item.name}: ${value}${contentType}`; } }) .join('\n') diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index afb7ca3f9..594d02e97 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -103,18 +103,21 @@ ], "multipartForm": [ { + "contentType": "", "name": "apikey", "value": "secret", "enabled": true, "type": "text" }, { + "contentType": "", "name": "numbers", "value": "+91998877665", "enabled": true, "type": "text" }, { + "contentType": "", "name": "message", "value": "hello", "enabled": false, diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 033e68277..c0cacc1c4 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -68,6 +68,7 @@ const multipartFormSchema = Yup.object({ otherwise: Yup.string().nullable() }), description: Yup.string().nullable(), + contentType: Yup.string().nullable(), enabled: Yup.boolean() }) .noUnknown(true) diff --git a/packages/bruno-tests/collection/multipart/mixed-content-types.bru b/packages/bruno-tests/collection/multipart/mixed-content-types.bru new file mode 100644 index 000000000..7fc421100 --- /dev/null +++ b/packages/bruno-tests/collection/multipart/mixed-content-types.bru @@ -0,0 +1,39 @@ +meta { + name: mixed-content-types + type: http + seq: 1 +} + +post { + url: {{host}}/api/multipart/mixed-content-types + body: multipartForm + auth: none +} + +body:multipart-form { + param1: test + param2: {"test":"i am json"} (Content-Type=application/json) + param3: @file(multipart/small.png) +} + +tests { + test("Status code is 200", function () { + expect(res.getStatus()).to.equal(200); + }); + test("param1 has no content-type", function () { + var param1 = res.body.find(p=>p.name === 'param1') + expect(param1).to.be.an('object'); + expect(param1.contentType).to.be.undefined; + }); + test("param2 has content-type application/json", function () { + var param2 = res.body.find(p=>p.name === 'param2') + expect(param2).to.be.an('object'); + expect(param2.contentType).to.equals('application/json'); + }); + test("param3 has content-type image/png", function () { + var param3 = res.body.find(p=>p.name === 'param3') + expect(param3).to.be.an('object'); + expect(param3.contentType).to.equals('image/png'); + }); + +} diff --git a/packages/bruno-tests/collection/multipart/small.png b/packages/bruno-tests/collection/multipart/small.png new file mode 100644 index 0000000000000000000000000000000000000000..2b584adf0512bb703f04eab2f012893496a03684 GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^0zfRp!2~4lMxL|Nn{1`ISV`@iy0Uc%|V#aIAVSkP*Bp-#WBRfKlz8hlEhU99x!f{`uCrm;pK~% Vb1~=hq=1STJYD@<);T3K0RTE@AvOR2 literal 0 HcmV?d00001 diff --git a/packages/bruno-tests/src/index.js b/packages/bruno-tests/src/index.js index 9ba6e3170..d9b921951 100644 --- a/packages/bruno-tests/src/index.js +++ b/packages/bruno-tests/src/index.js @@ -2,23 +2,25 @@ const express = require('express'); const bodyParser = require('body-parser'); const xmlparser = require('express-xml-bodyparser'); const cors = require('cors'); -const multer = require('multer'); +const formDataParser = require('./multipart/form-data-parser'); const app = new express(); const port = process.env.PORT || 8080; -const upload = multer(); app.use(cors()); app.use(xmlparser()); app.use(bodyParser.text()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); +formDataParser.init(app, express); const authRouter = require('./auth'); const echoRouter = require('./echo'); +const multipartRouter = require('./multipart'); app.use('/api/auth', authRouter); app.use('/api/echo', echoRouter); +app.use('/api/multipart', multipartRouter); app.get('/ping', function (req, res) { return res.send('pong'); @@ -32,10 +34,6 @@ app.get('/query', function (req, res) { return res.json(req.query); }); -app.post('/echo/multipartForm', upload.none(), function (req, res) { - return res.json(req.body); -}); - app.get('/redirect-to-ping', function (req, res) { return res.redirect('/ping'); }); diff --git a/packages/bruno-tests/src/multipart/form-data-parser.js b/packages/bruno-tests/src/multipart/form-data-parser.js new file mode 100644 index 000000000..8b4aa500a --- /dev/null +++ b/packages/bruno-tests/src/multipart/form-data-parser.js @@ -0,0 +1,58 @@ +/** + * Instead of using multer for example to parse the multipart form data, we build our own parser + * so that we can verify the content type are set correctly by bruno (for example application/json for json content) + */ + +const extractParam = function (param, str, delimiter, quote, endDelimiter) { + let regex = new RegExp(`${param}${delimiter}\\s*${quote}(.*?)${quote}${endDelimiter}`); + const found = str.match(regex); + if (found != null && found.length > 1) { + return found[1]; + } else { + return null; + } +}; + +const init = function (app, express) { + app.use(express.raw({ type: 'multipart/form-data' })); +}; + +const parsePart = function (part) { + let result = {}; + const name = extractParam('name', part, '=', '"', ''); + if (name) { + result.name = name; + } + const filename = extractParam('filename', part, '=', '"', ''); + if (filename) { + result.filename = filename; + } + const contentType = extractParam('Content-Type', part, ':', '', ';'); + if (contentType) { + result.contentType = contentType; + } + if (!filename) { + result.value = part.substring(part.indexOf('value=') + 'value='.length); + } + if (contentType === 'application/json') { + result.value = JSON.parse(result.value); + } + return result; +}; + +const parse = function (req) { + const BOUNDARY = 'boundary='; + const contentType = req.headers['content-type']; + const boundary = '--' + contentType.substring(contentType.indexOf(BOUNDARY) + BOUNDARY.length); + const rawBody = req.body.toString(); + let parts = rawBody.split(boundary).filter((part) => part.length > 0); + parts = parts.map((part) => part.trim('\r\n')); + parts = parts.filter((part) => part != '--'); + parts = parts.map((part) => part.replace('\r\n\r\n', ';value=')); + parts = parts.map((part) => part.replace('\r\n', ';')); + parts = parts.map((part) => parsePart(part)); + return parts; +}; + +module.exports.parse = parse; +module.exports.init = init; diff --git a/packages/bruno-tests/src/multipart/index.js b/packages/bruno-tests/src/multipart/index.js new file mode 100644 index 000000000..a98837c54 --- /dev/null +++ b/packages/bruno-tests/src/multipart/index.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); +const formDataParser = require('./form-data-parser'); + +router.post('/mixed-content-types', (req, res) => { + const parts = formDataParser.parse(req); + return res.json(parts); +}); + +module.exports = router; From c3c91d61c818074c351397c41ce088bfb8073bf6 Mon Sep 17 00:00:00 2001 From: busy-panda Date: Fri, 19 Apr 2024 15:01:41 +0200 Subject: [PATCH 003/140] added placeholder support to MultiLineEditor component --- .../src/components/MultiLineEditor/StyledWrapper.js | 6 ++++++ .../bruno-app/src/components/MultiLineEditor/index.js | 1 + .../RequestPane/MultipartFormParams/StyledWrapper.js | 8 ++++++++ .../components/RequestPane/MultipartFormParams/index.js | 1 + packages/bruno-app/src/pages/Bruno/index.js | 1 + 5 files changed, 17 insertions(+) diff --git a/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js b/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js index 6b3a8d568..e8ef24de7 100644 --- a/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js +++ b/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js @@ -13,6 +13,12 @@ const StyledWrapper = styled.div` line-height: 30px; overflow: hidden; + pre.CodeMirror-placeholder { + color: ${(props) => props.theme.text}; + padding-left: 0; + opacity: 0.5; + } + .CodeMirror-scroll { overflow: hidden !important; ${'' /* padding-bottom: 50px !important; */} diff --git a/packages/bruno-app/src/components/MultiLineEditor/index.js b/packages/bruno-app/src/components/MultiLineEditor/index.js index efcd89f45..359dbc2ce 100644 --- a/packages/bruno-app/src/components/MultiLineEditor/index.js +++ b/packages/bruno-app/src/components/MultiLineEditor/index.js @@ -28,6 +28,7 @@ class MultiLineEditor extends Component { lineWrapping: false, lineNumbers: false, theme: this.props.theme === 'dark' ? 'monokai' : 'default', + placeholder: this.props.placeholder, mode: 'brunovariables', brunoVarInfo: { variables: getAllVariables(this.props.collection) diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js index 80a146a5c..49c45f21d 100644 --- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js @@ -24,6 +24,14 @@ const Wrapper = styled.div` width: 30%; } + &:nth-child(2) { + width: 45%; + } + + &:nth-child(3) { + width: 25%; + } + &:nth-child(4) { width: 70px; } diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js index 794e35add..4ddd64218 100644 --- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js @@ -151,6 +151,7 @@ const MultipartFormParams = ({ item, collection }) => { handleParamChange( diff --git a/packages/bruno-app/src/pages/Bruno/index.js b/packages/bruno-app/src/pages/Bruno/index.js index 71e24dcfa..d0b9c3485 100644 --- a/packages/bruno-app/src/pages/Bruno/index.js +++ b/packages/bruno-app/src/pages/Bruno/index.js @@ -31,6 +31,7 @@ if (!SERVER_RENDERED) { require('codemirror/addon/search/jump-to-line'); require('codemirror/addon/search/search'); require('codemirror/addon/search/searchcursor'); + require('codemirror/addon/display/placeholder'); require('codemirror/keymap/sublime'); require('codemirror-graphql/hint'); From 8e99ed32582ae423bbcbf0b692c375f927c17826 Mon Sep 17 00:00:00 2001 From: busy-panda Date: Fri, 19 Apr 2024 17:06:49 +0200 Subject: [PATCH 004/140] moved assertions from Tests panel to Assert panel and --- .../multipart/mixed-content-types.bru | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/packages/bruno-tests/collection/multipart/mixed-content-types.bru b/packages/bruno-tests/collection/multipart/mixed-content-types.bru index 7fc421100..45a1cdd18 100644 --- a/packages/bruno-tests/collection/multipart/mixed-content-types.bru +++ b/packages/bruno-tests/collection/multipart/mixed-content-types.bru @@ -16,24 +16,9 @@ body:multipart-form { param3: @file(multipart/small.png) } -tests { - test("Status code is 200", function () { - expect(res.getStatus()).to.equal(200); - }); - test("param1 has no content-type", function () { - var param1 = res.body.find(p=>p.name === 'param1') - expect(param1).to.be.an('object'); - expect(param1.contentType).to.be.undefined; - }); - test("param2 has content-type application/json", function () { - var param2 = res.body.find(p=>p.name === 'param2') - expect(param2).to.be.an('object'); - expect(param2.contentType).to.equals('application/json'); - }); - test("param3 has content-type image/png", function () { - var param3 = res.body.find(p=>p.name === 'param3') - expect(param3).to.be.an('object'); - expect(param3.contentType).to.equals('image/png'); - }); - +assert { + res.status: eq 200 + res.body.find(p=>p.name === 'param1').contentType: isUndefined + res.body.find(p=>p.name === 'param2').contentType: eq application/json + res.body.find(p=>p.name === 'param3').contentType: eq image/png } From b7f4edac249e0551cca1fdad15ebbd0adb11b1a1 Mon Sep 17 00:00:00 2001 From: busy-panda Date: Fri, 19 Apr 2024 17:08:38 +0200 Subject: [PATCH 005/140] Reduced the width of the Operator column in tab Assert --- .../components/RequestPane/Assertions/StyledWrapper.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js index db0a36e58..eef02664e 100644 --- a/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js @@ -21,7 +21,15 @@ const Wrapper = styled.div` padding: 6px 10px; &:nth-child(1) { - width: 30%; + width: 60%; + } + + &:nth-child(2) { + width: 130px; + } + + &:nth-child(3) { + width: 40%; } &:nth-child(4) { From 40872f6e9e2ecbd3e4851a349a659e933806ec53 Mon Sep 17 00:00:00 2001 From: busy-panda Date: Fri, 19 Apr 2024 17:14:44 +0200 Subject: [PATCH 006/140] Reduced the width of the Operator column in tab Assert --- .../components/RequestPane/Assertions/StyledWrapper.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js index eef02664e..8e658c263 100644 --- a/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js @@ -20,18 +20,10 @@ const Wrapper = styled.div` td { padding: 6px 10px; - &:nth-child(1) { - width: 60%; - } - &:nth-child(2) { width: 130px; } - &:nth-child(3) { - width: 40%; - } - &:nth-child(4) { width: 70px; } From a0fcb6c91f08fcb615d278850c1df35ca9680691 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 15 Oct 2024 14:15:06 +0530 Subject: [PATCH 007/140] refactor: GenerateCodeItem component to fix width issue --- .../Collection/CollectionItem/GenerateCodeItem/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js index aabed03dd..d00ad4aa6 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -48,7 +48,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => { return ( -
+
{languages && From b900d3070d9377780a29467f95b2341b81c20be6 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 15 Oct 2024 14:16:51 +0530 Subject: [PATCH 008/140] feat: tab switch for languages --- .../Collection/CollectionItem/GenerateCodeItem/index.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js index d00ad4aa6..cd99782d1 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -59,7 +59,16 @@ const GenerateCodeItem = ({ collection, item, onClose }) => { className={ language.name === selectedLanguage.name ? 'generate-code-item active' : 'generate-code-item' } + role="button" + tabIndex={0} onClick={() => setSelectedLanguage(language)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + setSelectedLanguage(language); + e.preventDefault(); + } + }} + aria-pressed={language.name === selectedLanguage.name} > {language.name}
From ca6c2ebb03b1be8a28a68b394ddacc57a374e3e1 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Wed, 16 Oct 2024 13:00:00 +0530 Subject: [PATCH 009/140] style: add logic to handle different screen sizes --- .../GenerateCodeItem/StyledWrapper.js | 22 +++++++++++++++++++ .../CollectionItem/GenerateCodeItem/index.js | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js index ca582a842..3d8ea1229 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js @@ -35,6 +35,28 @@ const StyledWrapper = styled.div` background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important; } } + + .flexible-container { + width: 100%; + } + + @media (max-width: 600px) { + .flexible-container { + width: 500px; + } + } + + @media (min-width: 601px) and (max-width: 1200px) { + .flexible-container { + width: 800px; + } + } + + @media (min-width: 1201px) { + .flexible-container { + width: 900px; + } + } `; export default StyledWrapper; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js index cd99782d1..f7ce85023 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -48,7 +48,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => { return ( -
+
{languages && From df206dc4d9fe6230016c38b8f8cca9e77924ccf4 Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Fri, 15 Nov 2024 10:38:16 +0530 Subject: [PATCH 010/140] fix: posthog api key as a process env var --- .../bruno-app/src/components/Sidebar/GoldenEdition/index.js | 2 +- packages/bruno-app/src/providers/App/useTelemetry.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js b/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js index 4335bc235..61c88d194 100644 --- a/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js +++ b/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js @@ -8,7 +8,7 @@ import StyledWrapper from './StyledWrapper'; import { useTheme } from 'providers/Theme/index'; let posthogClient = null; -const posthogApiKey = 'phc_7gtqSrrdZRohiozPMLIacjzgHbUlhalW1Bu16uYijMR'; +const posthogApiKey = process.env.POSTHOG_API_KEY; const getPosthogClient = () => { if (posthogClient) { return posthogClient; diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js index 2d79e0cab..1ecbb1230 100644 --- a/packages/bruno-app/src/providers/App/useTelemetry.js +++ b/packages/bruno-app/src/providers/App/useTelemetry.js @@ -13,7 +13,7 @@ import platformLib from 'platform'; import { uuid } from 'utils/common'; const { publicRuntimeConfig } = getConfig(); -const posthogApiKey = 'phc_7gtqSrrdZRohiozPMLIacjzgHbUlhalW1Bu16uYijMR'; +const posthogApiKey = process.env.POSTHOG_API_KEY; let posthogClient = null; const isPlaywrightTestRunning = () => { From d0ef70473d212d9d1c2d64d998040b25c6a378f5 Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Mon, 18 Nov 2024 13:39:55 +0530 Subject: [PATCH 011/140] fix: remove json bigint from cli --- packages/bruno-cli/package.json | 1 - .../bruno-cli/src/runner/prepare-request.js | 14 ++----- .../src/runner/run-single-request.js | 7 +++- packages/bruno-cli/src/utils/common.js | 29 ++++++++++++- .../collection/echo/echo bigint.bru | 42 ------------------- 5 files changed, 38 insertions(+), 55 deletions(-) delete mode 100644 packages/bruno-tests/collection/echo/echo bigint.bru diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json index 5020f8475..b182e4acc 100644 --- a/packages/bruno-cli/package.json +++ b/packages/bruno-cli/package.json @@ -37,7 +37,6 @@ "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", - "json-bigint": "^1.0.0", "lodash": "^4.17.21", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index bc2b22886..ffcae0360 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -1,6 +1,5 @@ const { get, each, filter } = require('lodash'); const fs = require('fs'); -var JSONbig = require('json-bigint'); const decomment = require('decomment'); const crypto = require('node:crypto'); @@ -31,7 +30,8 @@ const prepareRequest = (request, collectionRoot) => { method: request.method, url: request.url, headers: headers, - pathParams: request?.params?.filter((param) => param.type === 'path') + pathParams: request?.params?.filter((param) => param.type === 'path'), + responseType: 'arraybuffer' }; const collectionAuth = get(collectionRoot, 'request.auth'); @@ -96,16 +96,10 @@ const prepareRequest = (request, collectionRoot) => { if (!contentTypeDefined) { axiosRequest.headers['content-type'] = 'application/json'; } - let jsonBody; try { - jsonBody = decomment(request?.body?.json); + axiosRequest.data = decomment(request?.body?.json); } catch (error) { - jsonBody = request?.body?.json; - } - try { - axiosRequest.data = JSONbig.parse(jsonBody); - } catch (error) { - axiosRequest.data = jsonBody; + axiosRequest.data = request?.body?.json; } } diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index cb59c78ba..0705f5270 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -19,7 +19,7 @@ const { makeAxiosInstance } = require('../utils/axios-instance'); const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper'); const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util'); const path = require('path'); -const { createFormData } = require('../utils/common'); +const { createFormData, parseDataFromResponse } = require('../utils/common'); const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; const onConsoleLog = (type, args) => { @@ -217,11 +217,16 @@ const runSingleRequest = async function ( /** @type {import('axios').AxiosResponse} */ response = await axiosInstance(request); + const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson); + response.data = data; + // Prevents the duration on leaking to the actual result responseTime = response.headers.get('request-duration'); response.headers.delete('request-duration'); } catch (err) { if (err?.response) { + const { data } = parseDataFromResponse(err?.response); + err.response.data = data; response = err.response; // Prevents the duration on leaking to the actual result diff --git a/packages/bruno-cli/src/utils/common.js b/packages/bruno-cli/src/utils/common.js index 16c2d1a7b..dd45be501 100644 --- a/packages/bruno-cli/src/utils/common.js +++ b/packages/bruno-cli/src/utils/common.js @@ -2,6 +2,7 @@ const fs = require('fs'); const FormData = require('form-data'); const { forOwn } = require('lodash'); const path = require('path'); +const iconv = require('iconv-lite'); const lpad = (str, width) => { let paddedStr = str; @@ -43,9 +44,35 @@ const createFormData = (datas, collectionPath) => { return form; }; +const parseDataFromResponse = (response, disableParsingResponseJson = false) => { + // Parse the charset from content type: https://stackoverflow.com/a/33192813 + const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || ''); + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals + const charsetValue = charsetMatch?.[1]; + const dataBuffer = Buffer.from(response.data); + // Overwrite the original data for backwards compatibility + let data; + if (iconv.encodingExists(charsetValue)) { + data = iconv.decode(dataBuffer, charsetValue); + } else { + data = iconv.decode(dataBuffer, 'utf-8'); + } + // Try to parse response to JSON, this can quietly fail + try { + // Filter out ZWNBSP character + // https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d + data = data.replace(/^\uFEFF/, ''); + if (!disableParsingResponseJson) { + data = JSON.parse(data); + } + } catch { } + + return { data, dataBuffer }; +}; module.exports = { lpad, rpad, - createFormData + createFormData, + parseDataFromResponse }; diff --git a/packages/bruno-tests/collection/echo/echo bigint.bru b/packages/bruno-tests/collection/echo/echo bigint.bru deleted file mode 100644 index ef981c723..000000000 --- a/packages/bruno-tests/collection/echo/echo bigint.bru +++ /dev/null @@ -1,42 +0,0 @@ -meta { - name: echo bigint - type: http - seq: 6 -} - -post { - url: {{host}}/api/echo/json - body: json - auth: none -} - -headers { - foo: bar -} - -auth:basic { - username: asd - password: j -} - -auth:bearer { - token: -} - -body:json { - { - "hello": 990531470713421825, - "decimal": 1.0, - "decimal2": 1.00, - "decimal3": 1.00200, - "decimal4": 0.00 - } -} - -assert { - res.status: eq 200 -} - -tests { - // todo: add tests once lossless json echo server is ready -} From b206b70d2e225ce4d3a672c07f50520d14e97f71 Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Tue, 19 Nov 2024 12:36:20 +0530 Subject: [PATCH 012/140] feat: fix tests --- packages/bruno-cli/tests/runner/prepare-request.spec.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/bruno-cli/tests/runner/prepare-request.spec.js b/packages/bruno-cli/tests/runner/prepare-request.spec.js index 6e2219af8..3c722ab4f 100644 --- a/packages/bruno-cli/tests/runner/prepare-request.spec.js +++ b/packages/bruno-cli/tests/runner/prepare-request.spec.js @@ -6,14 +6,18 @@ describe('prepare-request: prepareRequest', () => { describe('Decomments request body', () => { it('If request body is valid JSON', async () => { const body = { mode: 'json', json: '{\n"test": "{{someVar}}" // comment\n}' }; - const expected = { test: '{{someVar}}' }; + const expected = `{ +\"test\": \"{{someVar}}\" +}`; const result = prepareRequest({ 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 expected = `{ +\"test\": {{someVar}} +}`; const result = prepareRequest({ body }); expect(result.data).toEqual(expected); }); From 4a4481a26f3ec18e96aa4aaba89db00f94f60fa0 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar Date: Thu, 21 Nov 2024 12:00:25 +0530 Subject: [PATCH 013/140] feat: add 'isNotEmpty' assertion operator --- .../Assertions/AssertionOperator/index.js | 2 ++ .../RequestPane/Assertions/AssertionRow/index.js | 4 ++++ packages/bruno-js/src/runtime/assert-runtime.js | 12 ++++++++++++ 3 files changed, 18 insertions(+) diff --git a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionOperator/index.js b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionOperator/index.js index 1bc1c3f88..523fcf73d 100644 --- a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionOperator/index.js +++ b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionOperator/index.js @@ -20,6 +20,7 @@ import React from 'react'; * endsWith : ends with * between : between * isEmpty : is empty + * isNotEmpty : is not empty * isNull : is null * isUndefined : is undefined * isDefined : is defined @@ -51,6 +52,7 @@ const AssertionOperator = ({ operator, onChange }) => { 'endsWith', 'between', 'isEmpty', + 'isNotEmpty', 'isNull', 'isUndefined', 'isDefined', diff --git a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js index 38250f8ea..953db9e8d 100644 --- a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js +++ b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js @@ -24,6 +24,7 @@ import { useTheme } from 'providers/Theme'; * endsWith : ends with * between : between * isEmpty : is empty + * isNotEmpty : is not empty * isNull : is null * isUndefined : is undefined * isDefined : is defined @@ -61,6 +62,7 @@ const parseAssertionOperator = (str = '') => { 'endsWith', 'between', 'isEmpty', + 'isNotEmpty', 'isNull', 'isUndefined', 'isDefined', @@ -75,6 +77,7 @@ const parseAssertionOperator = (str = '') => { const unaryOperators = [ 'isEmpty', + 'isNotEmpty', 'isNull', 'isUndefined', 'isDefined', @@ -113,6 +116,7 @@ const parseAssertionOperator = (str = '') => { const isUnaryOperator = (operator) => { const unaryOperators = [ 'isEmpty', + 'isNotEmpty', 'isNull', 'isUndefined', 'isDefined', diff --git a/packages/bruno-js/src/runtime/assert-runtime.js b/packages/bruno-js/src/runtime/assert-runtime.js index b338730cc..d62bd22bf 100644 --- a/packages/bruno-js/src/runtime/assert-runtime.js +++ b/packages/bruno-js/src/runtime/assert-runtime.js @@ -58,6 +58,7 @@ chai.use(function (chai, utils) { * endsWith : ends with * between : between * isEmpty : is empty + * isNotEmpty : is not empty * isNull : is null * isUndefined : is undefined * isDefined : is defined @@ -95,6 +96,7 @@ const parseAssertionOperator = (str = '') => { 'endsWith', 'between', 'isEmpty', + 'isNotEmpty', 'isNull', 'isUndefined', 'isDefined', @@ -109,6 +111,7 @@ const parseAssertionOperator = (str = '') => { const unaryOperators = [ 'isEmpty', + 'isNotEmpty', 'isNull', 'isUndefined', 'isDefined', @@ -147,6 +150,7 @@ const parseAssertionOperator = (str = '') => { const isUnaryOperator = (operator) => { const unaryOperators = [ 'isEmpty', + 'isNotEmpty', 'isNull', 'isUndefined', 'isDefined', @@ -345,6 +349,14 @@ class AssertRuntime { case 'isEmpty': expect(lhs).to.be.empty; break; + case 'isNotEmpty': + expect(lhs).to.not.be.oneOf([null, 0, false, '', []]); + if (Array.isArray(lhs)) { + expect(lhs).to.have.length.above(0); + } else if (typeof lhs === 'object' && lhs !== null) { + expect(Object.keys(lhs)).to.have.length.above(0); + } + break; case 'isNull': expect(lhs).to.be.null; break; From 5b6172e5ac64d6744a9d564e48d9cb68cb15da80 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 3 Dec 2024 17:59:48 +0530 Subject: [PATCH 014/140] feat: enhance keyboard navigation for language selection in GenerateCodeItem --- .../CollectionItem/GenerateCodeItem/index.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js index f7ce85023..ef5143551 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -63,9 +63,20 @@ const GenerateCodeItem = ({ collection, item, onClose }) => { tabIndex={0} onClick={() => setSelectedLanguage(language)} onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - setSelectedLanguage(language); + if (e.key === 'Tab') { e.preventDefault(); + const currentIndex = languages.findIndex((lang) => lang.name === selectedLanguage.name); + const nextIndex = e.shiftKey + ? (currentIndex - 1 + languages.length) % languages.length + : (currentIndex + 1) % languages.length; + setSelectedLanguage(languages[nextIndex]); + } + + if (e.shiftKey && e.key === 'Tab') { + e.preventDefault(); + const currentIndex = languages.findIndex((lang) => lang.name === selectedLanguage.name); + const nextIndex = (currentIndex - 1 + languages.length) % languages.length; + setSelectedLanguage(languages[nextIndex]); } }} aria-pressed={language.name === selectedLanguage.name} From f110d898f5ad450d283d1c7e4978a86189f7c3aa Mon Sep 17 00:00:00 2001 From: Sanjai Kumar Date: Tue, 3 Dec 2024 18:01:22 +0530 Subject: [PATCH 015/140] fix: refactor createFormData function --- .../src/ipc/network/prepare-request.js | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index c8b36bb89..80c3ee64d 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -1,5 +1,5 @@ const os = require('os'); -const { get, each, filter, compact, forOwn } = require('lodash'); +const { get, each, filter, compact, forEach } = require('lodash'); const decomment = require('decomment'); const FormData = require('form-data'); const fs = require('fs'); @@ -166,26 +166,34 @@ const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => { } }; -const createFormData = (datas, collectionPath) => { +const createFormData = (data, collectionPath) => { // make axios work in node using form data // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427 const form = new FormData(); - forOwn(datas, (value, key) => { - if (typeof value == 'string') { - form.append(key, value); + forEach(data, (datum) => { + const { name, type, value } = datum; + + if (type === 'text') { + if (Array.isArray(value)) { + value.forEach((val) => form.append(name, val)); + } else { + form.append(name, value); + } return; } - const filePaths = value || []; - filePaths?.forEach?.((filePath) => { - let trimmedFilePath = filePath.trim(); + if (type === 'file') { + const filePaths = value || []; + filePaths.forEach((filePath) => { + let trimmedFilePath = filePath.trim(); - if (!path.isAbsolute(trimmedFilePath)) { - trimmedFilePath = path.join(collectionPath, trimmedFilePath); - } + if (!path.isAbsolute(trimmedFilePath)) { + trimmedFilePath = path.join(collectionPath, trimmedFilePath); + } - form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); - }); + form.append(name, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); + }); + } }); return form; }; @@ -435,11 +443,11 @@ const prepareRequest = (item, collection) => { } if (request.body.mode === 'multipartForm') { - axiosRequest.headers['content-type'] = 'multipart/form-data'; - const params = {}; + if (!contentTypeDefined) { + axiosRequest.headers['content-type'] = 'multipart/form-data'; + } const enabledParams = filter(request.body.multipartForm, (p) => p.enabled); - each(enabledParams, (p) => (params[p.name] = p.value)); - axiosRequest.data = params; + axiosRequest.data = createFormData(enabledParams); } if (request.body.mode === 'graphql') { From 3fe0d43bdc25daf568fba1cf656a3ff2c6083ce7 Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Fri, 6 Dec 2024 22:58:02 +0530 Subject: [PATCH 016/140] fix: cli - collection level script, added tests --- .../bruno-cli/src/runner/prepare-request.js | 26 +++++++---------- .../bruno-tests/collection/collection.bru | 22 +++++++++++++++ .../js/folder-collection script-tests pre.bru | 16 +++++++++++ .../js/folder-collection script-tests.bru | 28 +++++++++++++++++++ .../collection/scripting/js/folder.bru | 28 +++++++++++++++++++ 5 files changed, 104 insertions(+), 16 deletions(-) create mode 100644 packages/bruno-tests/collection/scripting/js/folder-collection script-tests pre.bru create mode 100644 packages/bruno-tests/collection/scripting/js/folder-collection script-tests.bru create mode 100644 packages/bruno-tests/collection/scripting/js/folder.bru diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 1113ed6f0..8c9daa437 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -143,24 +143,18 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => { } } - if (combinedPreReqScript.length) { - request.script.req = compact([collectionPreReqScript, ...combinedPreReqScript, request?.script?.req || '']).join(os.EOL); + request.script.req = compact([collectionPreReqScript, ...combinedPreReqScript, request?.script?.req || '']).join(os.EOL); + + if (scriptFlow === 'sequential') { + request.script.res = compact([collectionPostResScript, ...combinedPostResScript, request?.script?.res || '']).join(os.EOL); + } else { + request.script.res = compact([request?.script?.res || '', ...combinedPostResScript.reverse(), collectionPostResScript]).join(os.EOL); } - if (combinedPostResScript.length) { - if (scriptFlow === 'sequential') { - request.script.res = compact([collectionPostResScript, ...combinedPostResScript, request?.script?.res || '']).join(os.EOL); - } else { - request.script.res = compact([request?.script?.res || '', ...combinedPostResScript.reverse(), collectionPostResScript]).join(os.EOL); - } - } - - if (combinedTests.length) { - if (scriptFlow === 'sequential') { - request.tests = compact([collectionTests, ...combinedTests, request?.tests || '']).join(os.EOL); - } else { - request.tests = compact([request?.tests || '', ...combinedTests.reverse(), collectionTests]).join(os.EOL); - } + if (scriptFlow === 'sequential') { + request.tests = compact([collectionTests, ...combinedTests, request?.tests || '']).join(os.EOL); + } else { + request.tests = compact([request?.tests || '', ...combinedTests.reverse(), collectionTests]).join(os.EOL); } }; diff --git a/packages/bruno-tests/collection/collection.bru b/packages/bruno-tests/collection/collection.bru index 32eb8a7af..6d84c2abe 100644 --- a/packages/bruno-tests/collection/collection.bru +++ b/packages/bruno-tests/collection/collection.bru @@ -1,6 +1,7 @@ headers { check: again token: {{collection_pre_var_token}} + collection-header: collection-header-value } auth { @@ -17,6 +18,27 @@ vars:pre-request { collection-var: collection-var-value } +script:pre-request { + // used by `scripting/js/folder-collection script-tests` + const shouldTestCollectionScripts = bru.getVar('should-test-collection-scripts'); + if(shouldTestCollectionScripts) { + bru.setVar('collection-var-set-by-collection-script', 'collection-var-value-set-by-collection-script'); + } +} + +tests { + // used by `scripting/js/folder-collection script-tests` + const shouldTestCollectionScripts = bru.getVar('should-test-collection-scripts'); + const collectionVar = bru.getVar("collection-var-set-by-collection-script"); + if (shouldTestCollectionScripts && collectionVar) { + test("collection level test - should get the var that was set by the collection script", function() { + expect(collectionVar).to.equal("collection-var-value-set-by-collection-script"); + }); + bru.setVar('collection-var-set-by-collection-script', null); + bru.setVar('should-test-collection-scripts', null); + } +} + docs { # bruno-testbench 🐶 diff --git a/packages/bruno-tests/collection/scripting/js/folder-collection script-tests pre.bru b/packages/bruno-tests/collection/scripting/js/folder-collection script-tests pre.bru new file mode 100644 index 000000000..52911a98d --- /dev/null +++ b/packages/bruno-tests/collection/scripting/js/folder-collection script-tests pre.bru @@ -0,0 +1,16 @@ +meta { + name: folder-collection script-tests pre + type: http + seq: 4 +} + +post { + url: {{echo-host}} + body: none + auth: none +} + +script:pre-request { + bru.setVar('should-test-collection-scripts', true); + bru.setVar('should-test-folder-scripts', true); +} diff --git a/packages/bruno-tests/collection/scripting/js/folder-collection script-tests.bru b/packages/bruno-tests/collection/scripting/js/folder-collection script-tests.bru new file mode 100644 index 000000000..7a892104f --- /dev/null +++ b/packages/bruno-tests/collection/scripting/js/folder-collection script-tests.bru @@ -0,0 +1,28 @@ +meta { + name: folder-collection script-tests + type: http + seq: 5 +} + +post { + url: {{echo-host}} + body: none + auth: none +} + +script:pre-request { + // do not delete - the collection/folder scripts/tests run during this request execution +} + +tests { + const collectionHeader = req.getHeader("collection-header"); + const folderHeader = req.getHeader("folder-header"); + + test("should get the header value set at collection level", function() { + expect(collectionHeader).to.equal("collection-header-value"); + }); + + test("should get the header value set at folder level", function() { + expect(folderHeader).to.equal("folder-header-value"); + }); +} diff --git a/packages/bruno-tests/collection/scripting/js/folder.bru b/packages/bruno-tests/collection/scripting/js/folder.bru new file mode 100644 index 000000000..bab33e1ba --- /dev/null +++ b/packages/bruno-tests/collection/scripting/js/folder.bru @@ -0,0 +1,28 @@ +meta { + name: js +} + +headers { + folder-header: folder-header-value +} + +script:pre-request { + // used by `scripting/js/folder-collection script-tests` + const shouldTestFolderScripts = bru.getVar('should-test-folder-scripts'); + if(shouldTestFolderScripts) { + bru.setVar('folder-var-set-by-folder-script', 'folder-var-value-set-by-folder-script'); + } +} + +tests { + // used by `scripting/js/folder-collection script-tests` + const shouldTestFolderScripts = bru.getVar('should-test-folder-scripts'); + const folderVar = bru.getVar("folder-var-set-by-folder-script"); + if (shouldTestFolderScripts && folderVar) { + test("folder level test - should get the var that was set by the folder script", function() { + expect(folderVar).to.equal("folder-var-value-set-by-folder-script"); + }); + bru.setVar('folder-var-set-by-folder-script', null); + bru.setVar('should-test-folder-scripts', null); + } +} From 85c6b2d97fe17a1836f557f7fcb16af906e15c9c Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Tue, 10 Dec 2024 12:57:03 +0530 Subject: [PATCH 017/140] fix: cli -- brutojson fn -- script obj --- packages/bruno-cli/src/utils/bru.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-cli/src/utils/bru.js b/packages/bruno-cli/src/utils/bru.js index 75707fef9..cb8cee7fe 100644 --- a/packages/bruno-cli/src/utils/bru.js +++ b/packages/bruno-cli/src/utils/bru.js @@ -58,7 +58,7 @@ const bruToJson = (bru) => { body: _.get(json, 'body', {}), vars: _.get(json, 'vars', []), assertions: _.get(json, 'assertions', []), - script: _.get(json, 'script', ''), + script: _.get(json, 'script', {}), tests: _.get(json, 'tests', '') } }; From a55ed9bd50da2e458b47dc2aa8ddfd62efa9e7a2 Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Thu, 12 Dec 2024 14:09:06 +0530 Subject: [PATCH 018/140] feat: cli -- system level proxy fix --- .../src/runner/run-single-request.js | 108 +++++++++++++----- .../bruno-cli/src/utils/axios-instance.js | 1 + packages/bruno-cli/src/utils/proxy-util.js | 13 ++- 3 files changed, 90 insertions(+), 32 deletions(-) diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 35cafb590..87ba7d2ff 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -17,7 +17,7 @@ const { HttpProxyAgent } = require('http-proxy-agent'); const { SocksProxyAgent } = require('socks-proxy-agent'); const { makeAxiosInstance } = require('../utils/axios-instance'); const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper'); -const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util'); +const { shouldUseProxy, PatchedHttpsProxyAgent, getSystemProxyEnvVariables } = require('../utils/proxy-util'); const path = require('path'); const { createFormData } = require('../utils/common'); const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies'); @@ -42,7 +42,7 @@ const runSingleRequest = async function ( try { let request; let nextRequestName; - let item = { + let item = { pathname: path.join(collectionPath, filename), ...bruJson } @@ -140,39 +140,85 @@ const runSingleRequest = async function ( } } - // set proxy if enabled - const proxyEnabled = get(brunoConfig, 'proxy.enabled', false); - const shouldProxy = shouldUseProxy(request.url, get(brunoConfig, 'proxy.bypassProxy', '')); - if (proxyEnabled && shouldProxy) { - const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions); - const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions); - const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions); - const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false); - const socksEnabled = proxyProtocol.includes('socks'); + let proxyMode = 'off'; + let proxyConfig = {}; - let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`; - let proxyUri; - if (proxyAuthEnabled) { - const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions); - const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions); - - proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`; - } else { - proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`; + const collectionProxyConfig = get(brunoConfig, 'proxy', {}); + const collectionProxyEnabled = get(collectionProxyConfig, 'enabled', false); + if (collectionProxyEnabled === true) { + proxyConfig = collectionProxyConfig; + proxyMode = 'on'; + } else { + // if the collection level proxy is not set, pick the system level proxy by default, to maintain backward compatibility + const { http_proxy, https_proxy } = getSystemProxyEnvVariables(); + if (http_proxy?.length || https_proxy?.length) { + proxyMode = 'system'; } + } - if (socksEnabled) { - request.httpsAgent = new SocksProxyAgent( - proxyUri, - Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined - ); - request.httpAgent = new SocksProxyAgent(proxyUri); + if (proxyMode === 'on') { + const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', '')); + if (shouldProxy) { + const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions); + const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions); + const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions); + const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false); + const socksEnabled = proxyProtocol.includes('socks'); + let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`; + let proxyUri; + if (proxyAuthEnabled) { + const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions); + const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions); + + proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`; + } else { + proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`; + } + if (socksEnabled) { + request.httpsAgent = new SocksProxyAgent( + proxyUri, + Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined + ); + request.httpAgent = new SocksProxyAgent(proxyUri); + } else { + request.httpsAgent = new PatchedHttpsProxyAgent( + proxyUri, + Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined + ); + request.httpAgent = new HttpProxyAgent(proxyUri); + } } else { - request.httpsAgent = new PatchedHttpsProxyAgent( - proxyUri, - Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined - ); - request.httpAgent = new HttpProxyAgent(proxyUri); + request.httpsAgent = new https.Agent({ + ...httpsAgentRequestFields + }); + } + } else if (proxyMode === 'system') { + const { http_proxy, https_proxy, no_proxy } = getSystemProxyEnvVariables(); + const shouldUseSystemProxy = shouldUseProxy(request.url, no_proxy || ''); + if (shouldUseSystemProxy) { + try { + if (http_proxy?.length) { + new URL(http_proxy); + request.httpAgent = new HttpProxyAgent(http_proxy); + } + } catch (error) { + throw new Error('Invalid system http_proxy'); + } + try { + if (https_proxy?.length) { + new URL(https_proxy); + request.httpsAgent = new PatchedHttpsProxyAgent( + https_proxy, + Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined + ); + } + } catch (error) { + throw new Error('Invalid system https_proxy'); + } + } else { + request.httpsAgent = new https.Agent({ + ...httpsAgentRequestFields + }); } } else if (Object.keys(httpsAgentRequestFields).length > 0) { request.httpsAgent = new https.Agent({ diff --git a/packages/bruno-cli/src/utils/axios-instance.js b/packages/bruno-cli/src/utils/axios-instance.js index 8f61066c5..834cda2a8 100644 --- a/packages/bruno-cli/src/utils/axios-instance.js +++ b/packages/bruno-cli/src/utils/axios-instance.js @@ -10,6 +10,7 @@ const { CLI_VERSION } = require('../constants'); function makeAxiosInstance() { /** @type {axios.AxiosInstance} */ const instance = axios.create({ + proxy: false, headers: { "User-Agent": `bruno-runtime/${CLI_VERSION}` } diff --git a/packages/bruno-cli/src/utils/proxy-util.js b/packages/bruno-cli/src/utils/proxy-util.js index 729e03356..2a93d517e 100644 --- a/packages/bruno-cli/src/utils/proxy-util.js +++ b/packages/bruno-cli/src/utils/proxy-util.js @@ -79,7 +79,18 @@ class PatchedHttpsProxyAgent extends HttpsProxyAgent { } } + +const getSystemProxyEnvVariables = () => { + const { http_proxy, HTTP_PROXY, https_proxy, HTTPS_PROXY, no_proxy, NO_PROXY } = process.env; + return { + http_proxy: http_proxy || HTTP_PROXY, + https_proxy: https_proxy || HTTPS_PROXY, + no_proxy: no_proxy || NO_PROXY + }; +} + module.exports = { shouldUseProxy, - PatchedHttpsProxyAgent + PatchedHttpsProxyAgent, + getSystemProxyEnvVariables }; From 6326dc3d9c5bd39d527e8fe700bf5701d66ed950 Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Thu, 12 Dec 2024 17:28:15 +0530 Subject: [PATCH 019/140] feat: updates --- .../components/Sidebar/GoldenEdition/index.js | 2 +- .../src/providers/App/useTelemetry.js | 2 +- .../collection/echo/echo numbers.bru | 44 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 packages/bruno-tests/collection/echo/echo numbers.bru diff --git a/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js b/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js index 61c88d194..4335bc235 100644 --- a/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js +++ b/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js @@ -8,7 +8,7 @@ import StyledWrapper from './StyledWrapper'; import { useTheme } from 'providers/Theme/index'; let posthogClient = null; -const posthogApiKey = process.env.POSTHOG_API_KEY; +const posthogApiKey = 'phc_7gtqSrrdZRohiozPMLIacjzgHbUlhalW1Bu16uYijMR'; const getPosthogClient = () => { if (posthogClient) { return posthogClient; diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js index 1ecbb1230..2d79e0cab 100644 --- a/packages/bruno-app/src/providers/App/useTelemetry.js +++ b/packages/bruno-app/src/providers/App/useTelemetry.js @@ -13,7 +13,7 @@ import platformLib from 'platform'; import { uuid } from 'utils/common'; const { publicRuntimeConfig } = getConfig(); -const posthogApiKey = process.env.POSTHOG_API_KEY; +const posthogApiKey = 'phc_7gtqSrrdZRohiozPMLIacjzgHbUlhalW1Bu16uYijMR'; let posthogClient = null; const isPlaywrightTestRunning = () => { diff --git a/packages/bruno-tests/collection/echo/echo numbers.bru b/packages/bruno-tests/collection/echo/echo numbers.bru new file mode 100644 index 000000000..8f68fe558 --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo numbers.bru @@ -0,0 +1,44 @@ +meta { + name: echo numbers + type: http + seq: 1 +} + +post { + url: {{echo-host}} + body: json + auth: none +} + +body:json { + { + "integer": 123, + "negativeInteger": -99, + "zero": 0, + "float": 2.718, + "negativeFloat": -1.618, + "largeDouble": 12345.678901234567, + "smallDouble": 9.876e-12, + "booleanTrue": true, + "booleanFalse": false + } +} + +assert { + res.body.integer: eq 123 + res.body.integer: isNumber + res.body.negativeInteger: eq -99 + res.body.negativeInteger: isNumber + res.body.zero: eq 0 + res.body.zero: isNumber + res.body.float: eq 2.718 + res.body.float: isNumber + res.body.negativeFloat: eq -1.618 + res.body.negativeFloat: isNumber + res.body.largeDouble: eq 12345.678901234567 + res.body.largeDouble: isNumber + res.body.smallDouble: eq 9.876e-12 + res.body.smallDouble: isNumber + res.body.booleanTrue: eq true + res.body.booleanFalse: eq false +} From ea1f385d1cd8f5d8b0ed7cde4134193ea7abb97d Mon Sep 17 00:00:00 2001 From: lohit Date: Sat, 14 Dec 2024 18:55:28 +0530 Subject: [PATCH 020/140] fix: rename folder case insensitive (#3635) * fix: rename folder case insensitive * fix: add hasSubFolders utility and update folder moving logic for Windows OS * fix: validations * fix: updates * fix: updates --------- Co-authored-by: Pragadesh-45 --- packages/bruno-electron/src/app/watcher.js | 31 +++++++++++++++++++ packages/bruno-electron/src/ipc/collection.js | 27 +++++++++++++--- .../bruno-electron/src/utils/filesystem.js | 9 +++++- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index a26ff5d53..1b2e19d4a 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -44,6 +44,13 @@ const isCollectionRootBruFile = (pathname, collectionPath) => { return dirname === collectionPath && basename === 'collection.bru'; }; +const isPathInside = (childPath, parentPath) => { + const absoluteChildPath = path.resolve(childPath); + const absoluteParentPath = path.resolve(parentPath); + return absoluteChildPath.startsWith(absoluteParentPath); +} + + const hydrateRequestWithUuid = (request, pathname) => { request.uid = getRequestUid(pathname); @@ -506,6 +513,30 @@ class Watcher { this.watchers[watchPath] = null; } } + + getWatcherForPath(_path) { + const paths = Object.keys(this.watchers); + return paths.find(parentFolder => isPathInside(_path, parentFolder)); + } + + unlink(_path) { + const watcherPath = this.getWatcherForPath(_path); + if (watcherPath) { + const _watcher = this.watchers[watcherPath]; + _watcher.unwatch(_path); + } + } + + add(_path) { + const watcherPath = this.getWatcherForPath(_path); + if (watcherPath) { + const _watcher = this.watchers[watcherPath]; + if (!_watcher?.has?.(_path)) { + _watcher?.add?.(_path); + } + } + } + } module.exports = Watcher; diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 130228797..2c4ff3a84 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -21,7 +21,8 @@ const { normalizeAndResolvePath, safeToRename, isWindowsOS, - isValidFilename + isValidFilename, + hasSubFolders, } = require('../utils/filesystem'); const { openCollectionDialog } = require('../app/collections'); const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common'); @@ -343,6 +344,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // rename item ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => { + const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`); try { // Normalize paths if they are WSL paths oldPath = isWSLPath(oldPath) ? normalizeWslPath(oldPath) : normalizeAndResolvePath(oldPath); @@ -365,15 +367,18 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection moveRequestUid(bruFile, newBruFilePath); } - if (isWindowsOS() && !isWSLPath(oldPath)) { - const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`); + watcher.unlink(path.dirname(oldPath)); + if (isWindowsOS() && !isWSLPath(oldPath) && hasSubFolders(oldPath)) { + console.log('Windows OS: Moving folder with subfolders'); await fsExtra.copy(oldPath, tempDir); - await fsExtra.move(tempDir, newPath, { overwrite: true }); await fsExtra.remove(oldPath); + await fsExtra.move(tempDir, newPath, { overwrite: true }); + await fsExtra.remove(tempDir); } else { await fs.renameSync(oldPath, newPath); } + watcher.add(path.dirname(newPath)); return newPath; } @@ -397,6 +402,20 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection return newPath; } catch (error) { + // add path back to watcher in case an error during the rename file operations + // adds unlinked path back to watcher if doesn't exist + watcher.add(path.dirname(oldPath)); + + if (isWindowsOS() && !isWSLPath(oldPath)) { + if (fsExtra.pathExistsSync(tempDir) && !fsExtra.pathExistsSync(oldPath)) { + try { + await fsExtra.copy(tempDir, oldPath); + await fsExtra.remove(tempDir); + } catch (err) { + console.error("Failed to restore data to the old path:", err); + } + } + } return Promise.reject(error); } }); diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index aeb0bdd96..bd4e0aec0 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -38,6 +38,12 @@ const isDirectory = (dirPath) => { } }; +const hasSubFolders = (dir) => { + console.log('Checking for subfolders in:', dir); + const files = fs.readdirSync(dir); + return files.some(file => fs.statSync(path.join(dir, file)).isDirectory()); +}; + const normalizeAndResolvePath = (pathname) => { if (isSymbolicLink(pathname)) { const absPath = path.dirname(pathname); @@ -224,5 +230,6 @@ module.exports = { sanitizeDirectoryName, isWindowsOS, safeToRename, - isValidFilename + isValidFilename, + hasSubFolders }; From 3efcdf254ece8bd758abad3f8b23946a04f78f83 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Sat, 14 Dec 2024 19:42:39 +0530 Subject: [PATCH 021/140] fix(#3627): fix rename issues on windows os --- packages/bruno-electron/src/app/watcher.js | 44 +++++++++---------- packages/bruno-electron/src/ipc/collection.js | 41 ++++++++++++----- .../bruno-electron/src/utils/filesystem.js | 5 +-- 3 files changed, 54 insertions(+), 36 deletions(-) diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index 1b2e19d4a..43d01153d 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -44,13 +44,6 @@ const isCollectionRootBruFile = (pathname, collectionPath) => { return dirname === collectionPath && basename === 'collection.bru'; }; -const isPathInside = (childPath, parentPath) => { - const absoluteChildPath = path.resolve(childPath); - const absoluteParentPath = path.resolve(parentPath); - return absoluteChildPath.startsWith(absoluteParentPath); -} - - const hydrateRequestWithUuid = (request, pathname) => { request.uid = getRequestUid(pathname); @@ -396,6 +389,8 @@ const change = async (win, pathname, collectionUid, collectionPath) => { }; const unlink = (win, pathname, collectionUid, collectionPath) => { + console.log(`watcher unlink: ${pathname}`); + if (isBruEnvironmentConfig(pathname, collectionPath)) { return unlinkEnvironmentFile(win, pathname, collectionUid); } @@ -514,29 +509,32 @@ class Watcher { } } - getWatcherForPath(_path) { + getWatcherByItemPath(itemPath) { const paths = Object.keys(this.watchers); - return paths.find(parentFolder => isPathInside(_path, parentFolder)); + + const watcherPath = paths?.find(collectionPath => { + const absCollectionPath = path.resolve(collectionPath); + const absItemPath = path.resolve(itemPath); + + return absItemPath.startsWith(absCollectionPath); + }); + + return watcherPath ? this.watchers[watcherPath] : null; } - - unlink(_path) { - const watcherPath = this.getWatcherForPath(_path); - if (watcherPath) { - const _watcher = this.watchers[watcherPath]; - _watcher.unwatch(_path); + + unlinkItemPathInWatcher(itemPath) { + const watcher = this.getWatcherByItemPath(itemPath); + if (watcher) { + watcher.unwatch(itemPath); } } - add(_path) { - const watcherPath = this.getWatcherForPath(_path); - if (watcherPath) { - const _watcher = this.watchers[watcherPath]; - if (!_watcher?.has?.(_path)) { - _watcher?.add?.(_path); - } + addItemPathInWatcher(itemPath) { + const watcher = this.getWatcherByItemPath(itemPath); + if (watcher && !watcher?.has?.(itemPath)) { + watcher?.add?.(itemPath); } } - } module.exports = Watcher; diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 2c4ff3a84..8ca24ebcb 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -22,7 +22,7 @@ const { safeToRename, isWindowsOS, isValidFilename, - hasSubFolders, + hasSubDirectories, } = require('../utils/filesystem'); const { openCollectionDialog } = require('../app/collections'); const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common'); @@ -345,6 +345,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // rename item ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => { const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`); + const parentDir = path.dirname(oldPath); + const isWindowsOSAndNotWSLAndItemHasSubDirectories = isWindowsOS() && !isWSLPath(oldPath) && hasSubDirectories(oldPath); + let parentDirUnwatched = false; + let parentDirRewatched = false; + try { // Normalize paths if they are WSL paths oldPath = isWSLPath(oldPath) ? normalizeWslPath(oldPath) : normalizeAndResolvePath(oldPath); @@ -367,10 +372,19 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection moveRequestUid(bruFile, newBruFilePath); } - watcher.unlink(path.dirname(oldPath)); + watcher.unlinkItemPathInWatcher(parentDir); + parentDirUnwatched = true; - if (isWindowsOS() && !isWSLPath(oldPath) && hasSubFolders(oldPath)) { - console.log('Windows OS: Moving folder with subfolders'); + /** + * If it is windows OS + * And it is not WSL path (meaning its not linux running on windows using WSL) + * And it has sub directories + * Only then we need to use the temp dir approach to rename the folder + * + * Windows OS would sometimes throw error when renaming a folder with sub directories + * This is a alternative approach to avoid that error + */ + if (isWindowsOSAndNotWSLAndItemHasSubDirectories) { await fsExtra.copy(oldPath, tempDir); await fsExtra.remove(oldPath); await fsExtra.move(tempDir, newPath, { overwrite: true }); @@ -378,7 +392,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } else { await fs.renameSync(oldPath, newPath); } - watcher.add(path.dirname(newPath)); + watcher.addItemPathInWatcher(parentDir); + parentDirRewatched = true; + return newPath; } @@ -402,11 +418,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection return newPath; } catch (error) { - // add path back to watcher in case an error during the rename file operations - // adds unlinked path back to watcher if doesn't exist - watcher.add(path.dirname(oldPath)); - - if (isWindowsOS() && !isWSLPath(oldPath)) { + // in case an error occurs during the rename file operations after unlinking the parent dir + // and the rewatch fails, we need to add it back to watcher + if (parentDirUnwatched && !parentDirRewatched) { + watcher.addItemPathInWatcher(parentDir); + } + + // in case the rename file operations fails, and we see that the temp dir exists + // and the old path does not exist, we need to restore the data from the temp dir to the old path + if (isWindowsOSAndNotWSLAndItemHasSubDirectories) { if (fsExtra.pathExistsSync(tempDir) && !fsExtra.pathExistsSync(oldPath)) { try { await fsExtra.copy(tempDir, oldPath); @@ -416,6 +436,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } } } + return Promise.reject(error); } }); diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index bd4e0aec0..ec393bd51 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -38,8 +38,7 @@ const isDirectory = (dirPath) => { } }; -const hasSubFolders = (dir) => { - console.log('Checking for subfolders in:', dir); +const hasSubDirectories = (dir) => { const files = fs.readdirSync(dir); return files.some(file => fs.statSync(path.join(dir, file)).isDirectory()); }; @@ -231,5 +230,5 @@ module.exports = { isWindowsOS, safeToRename, isValidFilename, - hasSubFolders + hasSubDirectories }; From 33e86a909768ed1b378edf4d26225d0263e0997e Mon Sep 17 00:00:00 2001 From: lohit Date: Sat, 14 Dec 2024 19:57:04 +0530 Subject: [PATCH 022/140] feat: replace nextjs with rsbuild (#3617) * poc: bruno app rsbuild * fix: updates --------- Co-authored-by: Anoop M D --- packages/bruno-app/.gitignore | 2 +- packages/bruno-app/next.config.js | 22 -------- packages/bruno-app/package.json | 27 +++++---- packages/bruno-app/rsbuild.config.mjs | 27 +++++++++ .../bruno-app/src/components/Portal/index.js | 6 +- .../components/RequestPane/QueryUrl/index.js | 2 +- .../Collection/CollectionItem/index.js | 2 +- .../bruno-app/src/components/Table/index.js | 10 ++-- packages/bruno-app/src/index.js | 14 +++++ .../bruno-app/src/pages/{_app.js => Main.js} | 56 +++++-------------- packages/bruno-app/src/pages/_document.js | 41 -------------- packages/bruno-app/src/pages/index.js | 16 ++---- .../src/providers/App/useTelemetry.js | 6 +- .../src/providers/ReduxStore/index.js | 4 +- .../ReduxStore/slices/notifications.js | 1 - packages/bruno-graphql-docs/package.json | 2 +- scripts/build-electron.sh | 4 +- 17 files changed, 91 insertions(+), 151 deletions(-) delete mode 100644 packages/bruno-app/next.config.js create mode 100644 packages/bruno-app/rsbuild.config.mjs create mode 100644 packages/bruno-app/src/index.js rename packages/bruno-app/src/pages/{_app.js => Main.js} (62%) delete mode 100644 packages/bruno-app/src/pages/_document.js diff --git a/packages/bruno-app/.gitignore b/packages/bruno-app/.gitignore index 26d2023a3..691ec3051 100644 --- a/packages/bruno-app/.gitignore +++ b/packages/bruno-app/.gitignore @@ -31,6 +31,6 @@ yarn-error.log* # next.js .next/ -out/ +dist/ .env \ No newline at end of file diff --git a/packages/bruno-app/next.config.js b/packages/bruno-app/next.config.js deleted file mode 100644 index 209858093..000000000 --- a/packages/bruno-app/next.config.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - output: 'export', - reactStrictMode: false, - publicRuntimeConfig: { - CI: process.env.CI, - PLAYWRIGHT: process.env.PLAYWRIGHT, - ENV: process.env.ENV - }, - webpack: (config, { isServer }) => { - // Fixes npm packages that depend on `fs` module - if (!isServer) { - config.resolve.fallback.fs = false; - } - Object.defineProperty(config, 'devtool', { - get() { - return 'source-map'; - }, - set() {}, - }); - return config; - }, -}; diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index 52553b002..954e82b45 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -3,10 +3,9 @@ "version": "0.3.0", "private": true, "scripts": { - "dev": "cross-env ENV=dev next dev -p 3000", - "build": "next build", - "start": "next start", - "lint": "next lint", + "dev": "rsbuild dev --open", + "build": "rsbuild build -m production", + "preview": "rsbuild preview", "test": "jest", "test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"", "prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\"" @@ -49,7 +48,6 @@ "markdown-it-replace-link": "^1.2.0", "mousetrap": "^1.6.5", "nanoid": "3.3.4", - "next": "14.2.16", "path": "^0.12.7", "pdfjs-dist": "4.4.168", "platform": "^1.3.6", @@ -57,17 +55,17 @@ "prettier": "^2.7.1", "qs": "^6.11.0", "query-string": "^7.0.1", - "react": "18.2.0", + "react": "19.0.0", "react-copy-to-clipboard": "^5.1.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", - "react-dom": "18.2.0", + "react-dom": "19.0.0", "react-hot-toast": "^2.4.0", "react-i18next": "^15.0.1", "react-inspector": "^6.0.2", "react-pdf": "9.1.1", "react-player": "^2.16.0", - "react-redux": "^7.2.6", + "react-redux": "^7.2.9", "react-tooltip": "^5.5.2", "sass": "^1.46.0", "strip-json-comments": "^5.0.1", @@ -79,13 +77,14 @@ "yup": "^0.32.11" }, "devDependencies": { - "@babel/core": "^7.16.0", - "@babel/plugin-transform-spread": "^7.16.7", - "@babel/preset-env": "^7.16.4", - "@babel/preset-react": "^7.16.0", - "@babel/runtime": "^7.16.3", + "@rsbuild/core": "^1.1.2", + "@rsbuild/plugin-babel": "^1.0.3", + "@rsbuild/plugin-node-polyfill": "^1.2.0", + "@rsbuild/plugin-react": "^1.0.7", + "@rsbuild/plugin-sass": "^1.1.0", + "@rsbuild/plugin-styled-components": "1.1.0", "autoprefixer": "10.4.20", - "babel-loader": "^8.2.3", + "babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110", "cross-env": "^7.0.3", "css-loader": "7.1.2", "file-loader": "^6.2.0", diff --git a/packages/bruno-app/rsbuild.config.mjs b/packages/bruno-app/rsbuild.config.mjs new file mode 100644 index 000000000..704ea4d09 --- /dev/null +++ b/packages/bruno-app/rsbuild.config.mjs @@ -0,0 +1,27 @@ +import { defineConfig } from '@rsbuild/core'; +import { pluginReact } from '@rsbuild/plugin-react'; +import { pluginBabel } from '@rsbuild/plugin-babel'; +import { pluginStyledComponents } from '@rsbuild/plugin-styled-components'; +import { pluginSass } from '@rsbuild/plugin-sass'; +import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill' + +export default defineConfig({ + plugins: [ + pluginNodePolyfill(), + pluginReact(), + pluginStyledComponents(), + pluginSass(), + pluginBabel({ + include: /\.(?:js|jsx|tsx)$/, + babelLoaderOptions(opts) { + opts.plugins?.unshift('babel-plugin-react-compiler'); + } + }) + ], + source: { + tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file + }, + html: { + title: 'Bruno' + }, +}); diff --git a/packages/bruno-app/src/components/Portal/index.js b/packages/bruno-app/src/components/Portal/index.js index 4d34c0a04..7007c4766 100644 --- a/packages/bruno-app/src/components/Portal/index.js +++ b/packages/bruno-app/src/components/Portal/index.js @@ -1,8 +1,6 @@ import { createPortal } from 'react-dom'; -function Portal({ children, wrapperId }) { - wrapperId = wrapperId || 'bruno-app-body'; - - return createPortal(children, document.getElementById(wrapperId)); +function Portal({ children }) { + return createPortal(children, document.body); } export default Portal; diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js index f6d4ff308..9f3e600d0 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js @@ -70,7 +70,7 @@ const QueryUrl = ({ item, collection, handleRun }) => { const handleGenerateCode = (e) => { e.stopPropagation(); - if (item.request.url !== '' || (item.draft?.request.url !== undefined && item.draft?.request.url !== '')) { + if (item?.request?.url !== '' || (item.draft?.request?.url !== undefined && item.draft?.request?.url !== '')) { setGenerateCodeItemModalOpen(true); } else { toast.error('URL is required'); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 09b9552b5..fb380c20e 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -183,7 +183,7 @@ const CollectionItem = ({ item, collection, searchText }) => { const handleGenerateCode = (e) => { e.stopPropagation(); dropdownTippyRef.current.hide(); - if (item.request.url !== '' || (item.draft?.request.url !== undefined && item.draft?.request.url !== '')) { + if (item?.request?.url !== '' || (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')) { setGenerateCodeItemModalOpen(true); } else { toast.error('URL is required'); diff --git a/packages/bruno-app/src/components/Table/index.js b/packages/bruno-app/src/components/Table/index.js index 80bfb19f3..7c9b48d7d 100644 --- a/packages/bruno-app/src/components/Table/index.js +++ b/packages/bruno-app/src/components/Table/index.js @@ -63,16 +63,16 @@ const Table = ({ minColumnWidth = 1, headers = [], children }) => { [activeColumnIndex, columns, minColumnWidth] ); - const handleMouseUp = useCallback(() => { - setActiveColumnIndex(null); - removeListeners(); - }, [removeListeners]); - const removeListeners = useCallback(() => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', removeListeners); }, [handleMouseMove]); + const handleMouseUp = useCallback(() => { + setActiveColumnIndex(null); + removeListeners?.(); + }, [removeListeners]); + useEffect(() => { if (activeColumnIndex !== null) { window.addEventListener('mousemove', handleMouseMove); diff --git a/packages/bruno-app/src/index.js b/packages/bruno-app/src/index.js new file mode 100644 index 000000000..0e5187ebe --- /dev/null +++ b/packages/bruno-app/src/index.js @@ -0,0 +1,14 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './pages/index'; + +const rootElement = document.getElementById('root'); + +if (rootElement) { + const root = ReactDOM.createRoot(rootElement); + root.render( + + + + ); +} diff --git a/packages/bruno-app/src/pages/_app.js b/packages/bruno-app/src/pages/Main.js similarity index 62% rename from packages/bruno-app/src/pages/_app.js rename to packages/bruno-app/src/pages/Main.js index 08ebe6b0b..ba7b3289e 100644 --- a/packages/bruno-app/src/pages/_app.js +++ b/packages/bruno-app/src/pages/Main.js @@ -25,31 +25,7 @@ import '@fontsource/inter/900.css'; import { setupPolyfills } from 'utils/common/setupPolyfills'; setupPolyfills(); -function SafeHydrate({ children }) { - return
{typeof window === 'undefined' ? null : children}
; -} - -function NoSsr({ children }) { - const SERVER_RENDERED = typeof window === 'undefined'; - - if (SERVER_RENDERED) { - return null; - } - - return <>{children}; -} - -function MyApp({ Component, pageProps }) { - const [domLoaded, setDomLoaded] = useState(false); - - useEffect(() => { - setDomLoaded(true); - }, []); - - if (!domLoaded) { - return null; - } - +function Main({ children }) { if (!window.ipcRenderer) { return (