diff --git a/.gitignore b/.gitignore index 0da494ea2..b97cd17e3 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,5 @@ yarn-error.log* #dev editor bruno.iml -.idea \ No newline at end of file +.idea +.vscode \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..166ee6873 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,3 @@ +# .husky/pre-commit + +npx lint-staged \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..40f6c3351 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,41 @@ +// eslint.config.js +const { defineConfig } = require("eslint/config"); +const globals = require("globals"); + +module.exports = defineConfig([ + { + files: ["packages/bruno-app/**/*.{js,jsx,ts}"], + ignores: ["**/*.config.js"], + languageOptions: { + globals: { + ...globals.browser, + ...globals.jest, + global: false, + require: false, + Buffer: false, + process: false + }, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + rules: { + "no-undef": "error", + }, + }, + { + files: ["packages/bruno-electron/**/*.{js}"], + ignores: ["**/*.config.js"], + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + }, + rules: { + "no-undef": "error", + }, + } +]); diff --git a/package-lock.json b/package-lock.json index 08f122dc9..8b267dd3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,9 +27,12 @@ "@types/jest": "^29.5.11", "@types/lodash-es": "^4.17.12", "concurrently": "^8.2.2", + "eslint": "^9.26.0", "fs-extra": "^11.1.1", + "globals": "^16.1.0", "husky": "^8.0.3", "jest": "^29.2.0", + "lint-staged": "^15.5.2", "lodash-es": "^4.17.21", "pretty-quick": "^3.1.3", "randomstring": "^1.2.2", @@ -1704,9 +1707,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2009,6 +2012,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.26.0.tgz", + "integrity": "sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", @@ -2194,7 +2212,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2353,6 +2370,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", @@ -2476,6 +2502,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.26.5.tgz", + "integrity": "sha512-eGK26RsbIkYUns3Y8qKl362juDDYK+wEdPGHGrhzUl6CewZFo55VZ7hg+CyMFU4dd5QQakBN86nBMpRsFpRvbQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/plugin-syntax-flow": "^7.26.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", @@ -2950,7 +2992,6 @@ "version": "7.26.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.3.tgz", "integrity": "sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -3112,6 +3153,23 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-flow": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.25.9.tgz", + "integrity": "sha512-EASHsAhE+SSlEzJ4bzfusnXSHiU+JfAYzj+jbw2vgQKgq5HrUr8qs+vgtiEL5dOH6sEweI+PNt2D7AqrDSHyqQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-transform-flow-strip-types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", @@ -3130,7 +3188,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz", "integrity": "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -3146,6 +3203,56 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/register": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.25.9.tgz", + "integrity": "sha512-8D43jXtGsYmEeDvm4MWHYUpWf8iiXgWYx3fW7E7Wb7Oe6FWqJPl5K6TuFW0dOwNZzEE5rjlaSJYH9JjrUKJszA==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/@babel/runtime": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", @@ -3207,6 +3314,15 @@ } } }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/traverse/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3757,6 +3873,292 @@ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", "license": "MIT" }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", + "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@faker-js/faker": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", @@ -3949,6 +4351,72 @@ "react-dom": "^16 || ^17 || ^18" } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@iarna/toml": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", @@ -5011,6 +5479,44 @@ "node": ">=10" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz", + "integrity": "sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.3", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/@module-federation/runtime": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.5.1.tgz", @@ -8037,6 +8543,66 @@ "node": ">=6.0" } }, + "node_modules/@web/rollup-plugin-copy": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@web/rollup-plugin-copy/-/rollup-plugin-copy-0.5.1.tgz", + "integrity": "sha512-crDMXiT/Okn5nVQIEtXShB3NmGS9gQipD1s4kZwrTG+lu5TV665gfDzMFBzezo1e7ARTGd4d4p4EB0gy/JuNJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/rollup-plugin-copy/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@web/rollup-plugin-copy/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@web/rollup-plugin-copy/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -8319,6 +8885,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", @@ -8753,6 +9329,18 @@ "node": "*" } }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -10620,7 +11208,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4", @@ -10767,7 +11354,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true, "license": "MIT" }, "node_modules/compare-version": { @@ -11797,6 +12383,13 @@ "node": ">=6" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -12751,6 +13344,19 @@ "node": ">=4" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -12843,7 +13449,6 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=10" }, @@ -12851,6 +13456,69 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint": { + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", + "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.26.0", + "@eslint/plugin-kit": "^0.2.8", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@modelcontextprotocol/sdk": "^1.8.0", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "zod": "^3.24.2" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -12865,6 +13533,216 @@ "node": ">=8.0.0" } }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -12878,6 +13756,29 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -12977,6 +13878,29 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", + "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -13057,6 +13981,49 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/express-basic-auth": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz", @@ -13066,6 +14033,259 @@ "basic-auth": "^2.0.1" } }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/express/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/express/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/express/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/express/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -13168,6 +14388,13 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", @@ -13247,6 +14474,19 @@ "integrity": "sha512-KnYitqNf/rANEhUxWzkINAaMVc7SshejwA5HEd5Wr8lEJQX1Js1LCndectS44SXTnXWK+jbHQYs4R6CaG+7Jkg==", "license": "MIT" }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/file-loader": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", @@ -13336,6 +14576,124 @@ "node": ">= 0.8" } }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/find-cache-dir/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -13360,6 +14718,27 @@ "flat": "cli.js" } }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -13662,6 +15041,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -13856,12 +15248,16 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", + "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { @@ -14820,7 +16216,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -15137,6 +16532,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -16432,6 +17834,71 @@ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "license": "MIT" }, + "node_modules/jscodeshift": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-17.3.0.tgz", + "integrity": "sha512-LjFrGOIORqXBU+jwfC9nbkjmQfFldtMIoS6d9z2LG/lkmyNXsJAySPT+2SWXJEoE68/bCWcxKpXH37npftgmow==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/preset-flow": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@babel/register": "^7.24.6", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.7", + "neo-async": "^2.5.0", + "picocolors": "^1.0.1", + "recast": "^0.23.11", + "tmp": "^0.2.3", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + }, + "peerDependenciesMeta": { + "@babel/preset-env": { + "optional": true + } + } + }, + "node_modules/jscodeshift/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jscodeshift/node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/jsep": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", @@ -16552,6 +18019,13 @@ "integrity": "sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==", "license": "BSD-2-Clause" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -16747,7 +18221,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -16789,6 +18262,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -16824,6 +18311,409 @@ "uc.micro": "^1.0.1" } }, + "node_modules/lint-staged": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", + "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/lint-staged/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/lint-staged/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lint-staged/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lint-staged/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/load-script": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", @@ -16936,6 +18826,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -16983,6 +18880,222 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -17578,6 +19691,19 @@ "node": ">=8" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -17928,7 +20054,6 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, "license": "MIT" }, "node_modules/new-github-issue-url": { @@ -18435,6 +20560,24 @@ "node": ">=6" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -18889,6 +21032,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/path/node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", @@ -19011,6 +21164,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", @@ -19028,12 +21194,21 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -20021,6 +22196,16 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -20341,12 +22526,12 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", - "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -20954,6 +23139,22 @@ "node": ">=8.10.0" } }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, "node_modules/rechoir": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", @@ -21349,6 +23550,13 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rimraf": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", @@ -21630,6 +23838,48 @@ "dev": true, "license": "MIT" }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -22414,7 +24664,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, "license": "MIT", "dependencies": { "kind-of": "^6.0.2" @@ -22726,7 +24975,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -22745,7 +24993,6 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -23002,6 +25249,16 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-hash": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", @@ -23951,7 +26208,6 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", - "dev": true, "license": "MIT", "engines": { "node": ">=14.14" @@ -24163,6 +26419,19 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "license": "Unlicense" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -24845,6 +27114,16 @@ "dev": true, "license": "MIT" }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -25063,6 +27342,26 @@ "node": ">=10" } }, + "node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, "packages/bruno-app": { "name": "@usebruno/app", "version": "2.0.0", @@ -25222,6 +27521,7 @@ "dependencies": { "@aws-sdk/credential-providers": "3.750.0", "@usebruno/common": "0.1.0", + "@usebruno/converters": "^0.1.0", "@usebruno/js": "0.12.0", "@usebruno/lang": "0.12.0", "@usebruno/requests": "^0.1.0", @@ -25237,6 +27537,7 @@ "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "iconv-lite": "^0.6.3", + "js-yaml": "^4.1.0", "lodash": "^4.17.21", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", @@ -26824,6 +29125,16 @@ } } }, + "packages/bruno-common/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "packages/bruno-common/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -26851,6 +29162,7 @@ "dependencies": { "@usebruno/schema": "^0.7.0", "js-yaml": "^4.1.0", + "jscodeshift": "^17.3.0", "lodash": "^4.17.21", "nanoid": "3.3.8" }, @@ -26861,6 +29173,7 @@ "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^9.0.2", + "@web/rollup-plugin-copy": "^0.5.1", "babel-jest": "^29.7.0", "rimraf": "^5.0.7", "rollup": "3.2.5", @@ -26975,6 +29288,7 @@ "@aws-sdk/credential-providers": "3.750.0", "@faker-js/faker": "^9.5.1", "@usebruno/common": "0.1.0", + "@usebruno/converters": "^0.1.0", "@usebruno/js": "0.12.0", "@usebruno/lang": "0.12.0", "@usebruno/node-machine-id": "^2.0.0", @@ -28183,6 +30497,7 @@ "node-vault": "^0.10.2", "path": "^0.12.7", "quickjs-emscripten": "^0.29.2", + "tv4": "^1.3.0", "uuid": "^9.0.0", "xml2js": "^0.6.2" }, diff --git a/package.json b/package.json index 7508cb904..30d41bb01 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,12 @@ "@types/jest": "^29.5.11", "@types/lodash-es": "^4.17.12", "concurrently": "^8.2.2", + "eslint": "^9.26.0", "fs-extra": "^11.1.1", + "globals": "^16.1.0", "husky": "^8.0.3", "jest": "^29.2.0", + "lint-staged": "^15.5.2", "lodash-es": "^4.17.21", "pretty-quick": "^3.1.3", "randomstring": "^1.2.2", @@ -59,7 +62,8 @@ "test:e2e": "npx playwright test", "test:report": "npx playwright show-report", "test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app", - "prepare": "husky install" + "prepare": "husky install", + "lint": "npx eslint ./" }, "overrides": { "rollup": "3.29.5", @@ -68,5 +72,8 @@ "json-schema-typed": "8.0.1" } } + }, + "lint-staged": { + "packages/**/*.{js,jsx,ts,tsx}": "npm run lint" } } diff --git a/packages/bruno-app/rsbuild.config.mjs b/packages/bruno-app/rsbuild.config.mjs index 25f5b7ae7..0a2e9081f 100644 --- a/packages/bruno-app/rsbuild.config.mjs +++ b/packages/bruno-app/rsbuild.config.mjs @@ -19,7 +19,7 @@ export default defineConfig({ }) ], source: { - tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file + tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file, }, html: { title: 'Bruno' @@ -34,6 +34,16 @@ export default defineConfig({ }, }, }, + ignoreWarnings: [ + (warning) => warning.message.includes('Critical dependency: the request of a dependency is an expression') && warning?.moduleDescriptor?.name?.includes('flow-parser') + ], + // Add externals configuration to exclude Node.js libraries + externals: { + // List specific Node.js modules you want to exclude + // Format: 'module-name': 'commonjs module-name' + 'worker_threads': 'commonjs worker_threads', + // 'path': 'commonjs path' + } }, } }); diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js index 751919cde..e08866ccf 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js @@ -72,7 +72,7 @@ const Info = ({ collection }) => { - {showShareCollectionModal && } + {showShareCollectionModal && } diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index ab2ea7691..c777aa85f 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react'; import cloneDeep from 'lodash/cloneDeep'; import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons'; import { useTheme } from 'providers/Theme'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions'; import SingleLineEditor from 'components/SingleLineEditor'; import StyledWrapper from './StyledWrapper'; @@ -13,11 +13,18 @@ import { variableNameRegex } from 'utils/common/regex'; import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions'; import toast from 'react-hot-toast'; import { Tooltip } from 'react-tooltip'; +import { getGlobalEnvironmentVariables } from 'utils/collections'; const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const addButtonRef = useRef(null); + const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments); + + let _collection = cloneDeep(collection); + + const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid }); + _collection.globalEnvironmentVariables = globalEnvironmentVariables; const formik = useFormik({ enableReinitialize: true, @@ -160,7 +167,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
{ tab = folderLevelSettingsSelectedTab[folder?.uid]; } - const folderRoot = collection?.items.find((item) => item.uid === folder?.uid)?.root; + const folderRoot = folder?.root; const hasScripts = folderRoot?.request?.script?.res || folderRoot?.request?.script?.req; const hasTests = folderRoot?.request?.tests; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/index.js index 9439a0bea..7692b5891 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/index.js @@ -1,6 +1,6 @@ +import { useState, useEffect, useMemo } from "react"; import { find } from "lodash"; import StyledWrapper from "./StyledWrapper"; -import { useState, useEffect } from "react"; import { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/icons'; import { getAllVariables } from 'utils/collections/index'; import { interpolate } from '@usebruno/common'; diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js index 9f3e600d0..321ed4fd5 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js @@ -140,7 +140,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
{generateCodeItemModalOpen && ( - setGenerateCodeItemModalOpen(false)} /> + setGenerateCodeItemModalOpen(false)} /> )} ); diff --git a/packages/bruno-app/src/components/RequestTabPanel/FolderNotFound/index.js b/packages/bruno-app/src/components/RequestTabPanel/FolderNotFound/index.js new file mode 100644 index 000000000..eff188890 --- /dev/null +++ b/packages/bruno-app/src/components/RequestTabPanel/FolderNotFound/index.js @@ -0,0 +1,42 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { closeTabs } from 'providers/ReduxStore/slices/tabs'; +import { useDispatch } from 'react-redux'; + +const FolderNotFound = ({ folderUid }) => { + const dispatch = useDispatch(); + const [showErrorMessage, setShowErrorMessage] = useState(false); + + const closeTab = useCallback(() => { + dispatch( + closeTabs({ + tabUids: [folderUid] + }) + ); + }, [dispatch, folderUid]); + + useEffect(() => { + setTimeout(() => { + setShowErrorMessage(true); + }, 300); + }, []); + + if (!showErrorMessage) { + return null; + } + + return ( +
+
+
Folder no longer exists.
+
+ This can happen when the folder was renamed or deleted on your filesystem. +
+
+ +
+ ); +}; + +export default FolderNotFound; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js index 90c6e2f41..5f53a5e02 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/index.js @@ -25,7 +25,7 @@ import { produce } from 'immer'; import CollectionOverview from 'components/CollectionSettings/Overview'; import RequestNotLoaded from './RequestNotLoaded'; import RequestIsLoading from './RequestIsLoading'; -import { closeTabs } from 'providers/ReduxStore/slices/tabs'; +import FolderNotFound from './FolderNotFound'; const MIN_LEFT_PANE_WIDTH = 300; const MIN_RIGHT_PANE_WIDTH = 350; @@ -165,11 +165,7 @@ const RequestTabPanel = () => { if (focusedTab.type === 'folder-settings') { const folder = findItemInCollection(collection, focusedTab.folderUid); if (!folder) { - dispatch( - closeTabs({ - tabUids: [activeTabUid] - }) - ); + return ; } return ; diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index 562fc319f..182b7ff7e 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -76,7 +76,9 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi className={`flex items-center justify-between tab-container px-1 ${tab.preview ? "italic" : ""}`} onMouseUp={handleMouseUp} // Add middle-click behavior here > - {tab.type === 'folder-settings' ? ( + {tab.type === 'folder-settings' && !folder ? ( + + ) : tab.type === 'folder-settings' ? ( dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} /> ) : ( dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} /> @@ -261,13 +263,14 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col return ( {showAddNewRequestModal && ( - setShowAddNewRequestModal(false)} /> + setShowAddNewRequestModal(false)} /> )} {showCloneRequestModal && ( setShowCloneRequestModal(false)} /> )} diff --git a/packages/bruno-app/src/components/RequestTabs/index.js b/packages/bruno-app/src/components/RequestTabs/index.js index d0cd0b459..1e1503a85 100644 --- a/packages/bruno-app/src/components/RequestTabs/index.js +++ b/packages/bruno-app/src/components/RequestTabs/index.js @@ -79,7 +79,7 @@ const RequestTabs = () => { return ( {newRequestModalOpen && ( - setNewRequestModalOpen(false)} /> + setNewRequestModalOpen(false)} /> )} {collectionRequestTabs && collectionRequestTabs.length ? ( <> diff --git a/packages/bruno-app/src/components/ResponsePane/ResponseSize/index.js b/packages/bruno-app/src/components/ResponsePane/ResponseSize/index.js index b956b0813..b1cff2157 100644 --- a/packages/bruno-app/src/components/ResponsePane/ResponseSize/index.js +++ b/packages/bruno-app/src/components/ResponsePane/ResponseSize/index.js @@ -2,14 +2,20 @@ import React from 'react'; import StyledWrapper from './StyledWrapper'; const ResponseSize = ({ size }) => { + + if (!Number.isFinite(size)) { + return null; + } + let sizeToDisplay = ''; + // If size is greater than 1024 bytes, format as KB if (size > 1024) { - // size is greater than 1kb let kb = Math.floor(size / 1024); let decimal = Math.round(((size % 1024) / 1024).toFixed(2) * 100); sizeToDisplay = kb + '.' + decimal + 'KB'; } else { + // If size is less than or equal to 1024 bytes, display as bytes (B) sizeToDisplay = size + 'B'; } diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index ebacf05c5..1fb120ae9 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -48,6 +48,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { }; const response = item.response || {}; + const responseSize = response.size || 0; const getTabPanel = (tab) => { switch (tab) { @@ -156,7 +157,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { - + ) : null} diff --git a/packages/bruno-app/src/components/ShareCollection/index.js b/packages/bruno-app/src/components/ShareCollection/index.js index 19f5f00be..d0db00905 100644 --- a/packages/bruno-app/src/components/ShareCollection/index.js +++ b/packages/bruno-app/src/components/ShareCollection/index.js @@ -7,8 +7,11 @@ import exportBrunoCollection from 'utils/collections/export'; import exportPostmanCollection from 'utils/exporters/postman-collection'; import { cloneDeep } from 'lodash'; import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index'; +import { useSelector } from 'react-redux'; +import { findCollectionByUid } from 'utils/collections/index'; -const ShareCollection = ({ onClose, collection }) => { +const ShareCollection = ({ onClose, collectionUid }) => { + const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid)); const handleExportBrunoCollection = () => { const collectionCopy = cloneDeep(collection); exportBrunoCollection(transformCollectionToSaveToExportAsFile(collectionCopy)); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js index ab9fc1a7f..a0a1e6c09 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js @@ -1,5 +1,5 @@ import React, { useRef, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions'; @@ -11,11 +11,13 @@ import Help from 'components/Help'; import PathDisplay from 'components/PathDisplay'; import { useState } from 'react'; import { IconArrowBackUp, IconEdit } from "@tabler/icons"; +import { findCollectionByUid } from 'utils/collections/index'; -const CloneCollection = ({ onClose, collection }) => { +const CloneCollection = ({ onClose, collectionUid }) => { const inputRef = useRef(); const dispatch = useDispatch(); const [isEditing, toggleEditing] = useState(false); + const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid)); const { name } = collection; const formik = useFormik({ @@ -46,7 +48,7 @@ const CloneCollection = ({ onClose, collection }) => { values.collectionName, values.collectionFolderName, values.collectionLocation, - collection.pathname + collection?.pathname ) ) .then(() => { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js index 9194e8a64..31a58c2dd 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js @@ -15,7 +15,7 @@ import Portal from 'components/Portal'; import Dropdown from 'components/Dropdown'; import StyledWrapper from './StyledWrapper'; -const CloneCollectionItem = ({ collection, item, onClose }) => { +const CloneCollectionItem = ({ collectionUid, collectionPathname, item, onClose }) => { const dispatch = useDispatch(); const isFolder = isItemAFolder(item); const inputRef = useRef(); @@ -49,7 +49,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => { .test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value)) }), onSubmit: (values) => { - dispatch(cloneItem(values.name, values.filename, item.uid, collection.uid)) + dispatch(cloneItem(values.name, values.filename, item.uid, collectionUid)) .then(() => { toast.success('Request cloned!'); onClose(); @@ -172,8 +172,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => { ) : (
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/StyledWrapper.js new file mode 100644 index 000000000..62f53069e --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/StyledWrapper.js @@ -0,0 +1,9 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + .drag-preview { + background-color: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/index.js new file mode 100644 index 000000000..1ad4065a8 --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/index.js @@ -0,0 +1,49 @@ +import { useDragLayer } from 'react-dnd'; +import { + IconFile, + IconFolder, +} from '@tabler/icons'; +import StyledWrapper from './StyledWrapper'; + +function getItemStyles({ x, y }) { + if (Number.isNaN(x) || Number.isNaN(y)) return { display: 'none' }; + const transform = `translate(${x}px, ${y}px)`; + + return { + position: 'fixed', + pointerEvents: 'none', + top: 0, + transform, + WebkitTransform: transform, + zIndex: 100, + }; +} + +export const CollectionItemDragPreview = () => { + const { + item, + isDragging, + clientOffset + } = useDragLayer((monitor) => ({ + item: monitor.getItem(), + isDragging: monitor.isDragging(), + clientOffset: monitor.getClientOffset(), + })); + if (!isDragging) return null; + const { x, y } = clientOffset || {}; + const shouldShowFolderIcon = !item.type || item.type === 'folder'; + return ( + +
+
+ {shouldShowFolderIcon ? ( + + ) : ( + + )} + {item.name} +
+
+
+ ); +}; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/DeleteCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/DeleteCollectionItem/index.js index 2646bf676..3f397c78c 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/DeleteCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/DeleteCollectionItem/index.js @@ -7,11 +7,11 @@ import { deleteItem } from 'providers/ReduxStore/slices/collections/actions'; import { recursivelyGetAllItemUids } from 'utils/collections'; import StyledWrapper from './StyledWrapper'; -const DeleteCollectionItem = ({ onClose, item, collection }) => { +const DeleteCollectionItem = ({ onClose, item, collectionUid }) => { const dispatch = useDispatch(); const isFolder = isItemAFolder(item); const onConfirm = () => { - dispatch(deleteItem(item.uid, collection.uid)).then(() => { + dispatch(deleteItem(item.uid, collectionUid)).then(() => { if (isFolder) { // close all tabs that belong to the folder diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js index 3092df4ba..ea3ed43a7 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js @@ -62,6 +62,7 @@ const CodeView = ({ language, item }) => { { +const GenerateCodeItem = ({ collectionUid, item, onClose }) => { const languages = getLanguages(); + const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid)); + const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments); const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid }); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js index 705c45c79..583a914b0 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js @@ -16,7 +16,7 @@ import Portal from 'components/Portal'; import Dropdown from 'components/Dropdown'; import StyledWrapper from './StyledWrapper'; -const RenameCollectionItem = ({ collection, item, onClose }) => { +const RenameCollectionItem = ({ collectionUid, collectionPathname, item, onClose }) => { const dispatch = useDispatch(); const isFolder = isItemAFolder(item); const inputRef = useRef(); @@ -57,13 +57,13 @@ const RenameCollectionItem = ({ collection, item, onClose }) => { return; } if (!isFolder && item.draft) { - await dispatch(saveRequest(item.uid, collection.uid, true)); + await dispatch(saveRequest(item.uid, collectionUid, true)); } const { name: newName, filename: newFilename } = values; try { let renameConfig = { itemUid: item.uid, - collectionUid: collection.uid, + collectionUid, }; renameConfig['newName'] = newName; if (itemFilename !== newFilename) { @@ -191,8 +191,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => { ) : (
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js index cfd236f8c..8aaaa749c 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js @@ -2,16 +2,18 @@ import React from 'react'; import get from 'lodash/get'; import { uuid } from 'utils/common'; import Modal from 'components/Modal'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { addTab } from 'providers/ReduxStore/slices/tabs'; import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions'; import { flattenItems } from 'utils/collections'; import StyledWrapper from './StyledWrapper'; import { areItemsLoading } from 'utils/collections'; -const RunCollectionItem = ({ collection, item, onClose }) => { +const RunCollectionItem = ({ collectionUid, item, onClose }) => { const dispatch = useDispatch(); + const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid)); + const onSubmit = (recursive) => { dispatch( addTab({ @@ -34,8 +36,6 @@ const RunCollectionItem = ({ collection, item, onClose }) => { const recursiveRunLength = getRequestsCount(flattenedItems); const isFolderLoading = areItemsLoading(item); - console.log(item); - console.log(isFolderLoading); return ( diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js index 8d61203e1..4e3525df9 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js @@ -1,6 +1,7 @@ import styled from 'styled-components'; const Wrapper = styled.div` + position: relative; .menu-icon { color: ${(props) => props.theme.sidebar.dropdownIcon.color}; @@ -22,6 +23,65 @@ const Wrapper = styled.div` height: 1.875rem; cursor: pointer; user-select: none; + position: relative; + + /* Common styles for drop indicators */ + &::before, + &::after { + content: ''; + position: absolute; + left: 0; + right: 0; + height: 2px; + background: ${(props) => props.theme.dragAndDrop.border}; + opacity: 0; + pointer-events: none; + } + + &::before { + top: 0; + } + + &::after { + bottom: 0; + } + + /* Drop target styles */ + &.drop-target { + background-color: ${(props) => props.theme.dragAndDrop.hoverBg}; + + &::before, + &::after { + opacity: 0; + } + } + + &.drop-target-above { + &::before { + opacity: 1; + height: 2px; + } + } + + &.drop-target-below { + &::after { + opacity: 1; + height: 2px; + } + } + + /* Inside drop target style */ + &.drop-target { + &::before { + top: 0; + bottom: 0; + height: 100%; + opacity: 1; + background: ${(props) => props.theme.dragAndDrop.hoverBg}; + border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border}; + // border-radius: 4px; + } + } .rotate-90 { transform: rotateZ(90deg); @@ -45,6 +105,20 @@ const Wrapper = styled.div` } } + &.item-target { + background: #ccc3; + } + + &.item-seperator { + .seperator { + bottom: 0px; + position: absolute; + height: 3px; + width: 100%; + background: #ccc3; + } + } + &.item-focused-in-tab { background: ${(props) => props.theme.sidebar.collection.item.bg}; 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 9b520f4a5..6c34dcaa3 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 @@ -1,4 +1,5 @@ import React, { useState, useRef, forwardRef, useEffect } from 'react'; +import { getEmptyImage } from 'react-dnd-html5-backend'; import range from 'lodash/range'; import filter from 'lodash/filter'; import classnames from 'classnames'; @@ -6,7 +7,7 @@ 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 { handleCollectionItemDrop, moveItem, sendRequest, showInFolder, updateItemsSequences } from 'providers/ReduxStore/slices/collections/actions'; import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections'; import Dropdown from 'components/Dropdown'; import NewRequest from 'components/Sidebar/NewRequest'; @@ -26,13 +27,22 @@ import NetworkError from 'components/ResponsePane/NetworkError/index'; import CollectionItemInfo from './CollectionItemInfo/index'; import CollectionItemIcon from './CollectionItemIcon'; import { scrollToTheActiveTab } from 'utils/tabs'; +import { isTabForItemActive as isTabForItemActiveSelector, isTabForItemPresent as isTabForItemPresentSelector } from 'src/selectors/tab'; +import { isEqual } from 'lodash'; +import { calculateDraggedItemNewPathname } from 'utils/collections/index'; -const CollectionItem = ({ item, collection, searchText }) => { - const tabs = useSelector((state) => state.tabs.tabs); - const activeTabUid = useSelector((state) => state.tabs.activeTabUid); +const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) => { + const _isTabForItemActiveSelector = isTabForItemActiveSelector({ itemUid: item.uid }); + const isTabForItemActive = useSelector(_isTabForItemActiveSelector, isEqual); + + const _isTabForItemPresentSelector = isTabForItemPresentSelector({ itemUid: item.uid }); + const isTabForItemPresent = useSelector(_isTabForItemPresentSelector, isEqual); + const isSidebarDragging = useSelector((state) => state.app.isDragging); const dispatch = useDispatch(); - const collectionItemRef = useRef(null); + + // We use a single ref for drag and drop. + const ref = useRef(null); const [renameItemModalOpen, setRenameItemModalOpen] = useState(false); const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false); @@ -44,10 +54,13 @@ const CollectionItem = ({ item, collection, searchText }) => { const [itemInfoModalOpen, setItemInfoModalOpen] = useState(false); const hasSearchText = searchText && searchText?.trim()?.length; const itemIsCollapsed = hasSearchText ? false : item.collapsed; + const isFolder = isItemAFolder(item); - const [{ isDragging }, drag] = useDrag({ - type: `collection-item-${collection.uid}`, - item: item, + const [dropType, setDropType] = useState(null); // 'adjacent' or 'inside' + + const [{ isDragging }, drag, dragPreview] = useDrag({ + type: `collection-item-${collectionUid}`, + item, collect: (monitor) => ({ isDragging: monitor.isDragging() }), @@ -56,21 +69,72 @@ const CollectionItem = ({ item, collection, searchText }) => { } }); - const [{ isOver }, drop] = useDrop({ - accept: `collection-item-${collection.uid}`, - drop: (draggedItem) => { - dispatch(moveItem(collection.uid, draggedItem.uid, item.uid)); + useEffect(() => { + dragPreview(getEmptyImage(), { captureDraggingState: true }); + }, []); + + const determineDropType = (monitor) => { + const hoverBoundingRect = ref.current?.getBoundingClientRect(); + const clientOffset = monitor.getClientOffset(); + if (!hoverBoundingRect || !clientOffset) return null; + + const clientY = clientOffset.y - hoverBoundingRect.top; + const folderUpperThreshold = hoverBoundingRect.height * 0.35; + const fileUpperThreshold = hoverBoundingRect.height * 0.5; + + if (isItemAFolder(item)) { + return clientY < folderUpperThreshold ? 'adjacent' : 'inside'; + } else { + return clientY < fileUpperThreshold ? 'adjacent' : null; + } + }; + + const canItemBeDropped = ({ draggedItem, targetItem, dropType }) => { + const { uid: targetItemUid, pathname: targetItemPathname } = targetItem; + const { uid: draggedItemUid, pathname: draggedItemPathname } = draggedItem; + + if (draggedItemUid === targetItemUid) return false; + + const newPathname = calculateDraggedItemNewPathname({ draggedItem, targetItem, dropType, collectionPathname }); + if (!newPathname) return false; + + if (targetItemPathname?.startsWith(draggedItemPathname)) return false; + + return true; + }; + + const [{ isOver, canDrop }, drop] = useDrop({ + accept: `collection-item-${collectionUid}`, + hover: (draggedItem, monitor) => { + const { uid: targetItemUid } = item; + const { uid: draggedItemUid } = draggedItem; + + if (draggedItemUid === targetItemUid) return; + + const dropType = determineDropType(monitor); + + const _canItemBeDropped = canItemBeDropped({ draggedItem, targetItem: item, dropType }); + + setDropType(_canItemBeDropped ? dropType : null); }, - canDrop: (draggedItem) => { - return draggedItem.uid !== item.uid; + drop: async (draggedItem, monitor) => { + const { uid: targetItemUid } = item; + const { uid: draggedItemUid } = draggedItem; + + if (draggedItemUid === targetItemUid) return; + + const dropType = determineDropType(monitor); + if (!dropType) return; + + await dispatch(handleCollectionItemDrop({ targetItem: item, draggedItem, dropType, collectionUid })) + setDropType(null); }, + canDrop: (draggedItem) => draggedItem.uid !== item.uid, collect: (monitor) => ({ - isOver: monitor.isOver(), + isOver: monitor.isOver() }), }); - drag(drop(collectionItemRef)); - const dropdownTippyRef = useRef(); const MenuIcon = forwardRef((props, ref) => { return ( @@ -84,13 +148,15 @@ const CollectionItem = ({ item, collection, searchText }) => { 'rotate-90': !itemIsCollapsed }); - const itemRowClassName = classnames('flex collection-item-name items-center', { - 'item-focused-in-tab': item.uid == activeTabUid, - 'item-hovered': isOver + const itemRowClassName = classnames('flex collection-item-name relative items-center', { + 'item-focused-in-tab': isTabForItemActive, + 'item-hovered': isOver && canDrop, + 'drop-target': isOver && dropType === 'inside', + 'drop-target-above': isOver && dropType === 'adjacent' }); const handleRun = async () => { - dispatch(sendRequest(item, collection.uid)).catch((err) => + dispatch(sendRequest(item, collectionUid)).catch((err) => toast.custom((t) => toast.dismiss(t.id)} />, { duration: 5000 }) @@ -101,12 +167,10 @@ const CollectionItem = ({ item, collection, searchText }) => { if (event && event.detail != 1) return; //scroll to the active tab setTimeout(scrollToTheActiveTab, 50); - const isRequest = isItemARequest(item); - if (isRequest) { dispatch(hideHomePage()); - if (itemIsOpenedInTabs(item, tabs)) { + if (isTabForItemPresent) { dispatch( focusTab({ uid: item.uid @@ -114,11 +178,10 @@ const CollectionItem = ({ item, collection, searchText }) => { ); return; } - dispatch( addTab({ uid: item.uid, - collectionUid: collection.uid, + collectionUid: collectionUid, requestPaneTab: getDefaultRequestPaneTab(item), type: 'request', }) @@ -127,14 +190,14 @@ const CollectionItem = ({ item, collection, searchText }) => { dispatch( addTab({ uid: item.uid, - collectionUid: collection.uid, + collectionUid: collectionUid, type: 'folder-settings', }) ); dispatch( collectionFolderClicked({ itemUid: item.uid, - collectionUid: collection.uid + collectionUid: collectionUid }) ); } @@ -146,10 +209,10 @@ const CollectionItem = ({ item, collection, searchText }) => { dispatch( collectionFolderClicked({ itemUid: item.uid, - collectionUid: collection.uid + collectionUid: collectionUid }) ); - } + }; const handleRightClick = (event) => { const _menuDropdown = dropdownTippyRef.current; @@ -164,7 +227,6 @@ const CollectionItem = ({ item, collection, searchText }) => { let indents = range(item.depth); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); - const isFolder = isItemAFolder(item); const className = classnames('flex flex-col w-full', { 'is-sidebar-dragging': isSidebarDragging @@ -183,49 +245,14 @@ const CollectionItem = ({ item, collection, searchText }) => { } const handleDoubleClick = (event) => { - dispatch(makeTabPermanent({ uid: item.uid })) + dispatch(makeTabPermanent({ uid: item.uid })); }; - // we need to sort request items by seq property - const sortRequestItems = (items = []) => { + // Sort items by their "seq" property. + const sortItemsBySequence = (items = []) => { return items.sort((a, b) => a.seq - b.seq); }; - // we need to sort folder items by name alphabetically - const sortFolderItems = (items = []) => { - return items.sort((a, b) => a.name.localeCompare(b.name)); - }; - const handleGenerateCode = (e) => { - e.stopPropagation(); - dropdownTippyRef.current.hide(); - if (item?.request?.url !== '' || (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')) { - setGenerateCodeItemModalOpen(true); - } else { - toast.error('URL is required'); - } - }; - - const viewFolderSettings = () => { - if (isItemAFolder(item)) { - if (itemIsOpenedInTabs(item, tabs)) { - dispatch( - focusTab({ - uid: item.uid - }) - ); - return; - } - dispatch( - addTab({ - uid: item.uid, - collectionUid: collection.uid, - type: 'folder-settings' - }) - ); - return; - } - }; - const handleShowInFolder = () => { dispatch(showInFolder(item.pathname)).catch((error) => { console.error('Error opening the folder', error); @@ -233,62 +260,89 @@ const CollectionItem = ({ item, collection, searchText }) => { }); }; - const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i))); - const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i))); + const folderItems = sortItemsBySequence(filter(item.items, (i) => isItemAFolder(i))); + const requestItems = sortItemsBySequence(filter(item.items, (i) => isItemARequest(i))); + + const handleGenerateCode = (e) => { + e.stopPropagation(); + dropdownTippyRef.current.hide(); + if ( + (item?.request?.url !== '') || + (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '') + ) { + setGenerateCodeItemModalOpen(true); + } else { + toast.error('URL is required'); + } + }; + + const viewFolderSettings = () => { + if (isItemAFolder(item)) { + if (isTabForItemPresent) { + dispatch(focusTab({ uid: item.uid })); + return; + } + dispatch( + addTab({ + uid: item.uid, + collectionUid, + type: 'folder-settings' + }) + ); + } + }; return ( {renameItemModalOpen && ( - setRenameItemModalOpen(false)} /> + setRenameItemModalOpen(false)} /> )} {cloneItemModalOpen && ( - setCloneItemModalOpen(false)} /> + setCloneItemModalOpen(false)} /> )} {deleteItemModalOpen && ( - setDeleteItemModalOpen(false)} /> + setDeleteItemModalOpen(false)} /> )} {newRequestModalOpen && ( - setNewRequestModalOpen(false)} /> + setNewRequestModalOpen(false)} /> )} {newFolderModalOpen && ( - setNewFolderModalOpen(false)} /> + setNewFolderModalOpen(false)} /> )} {runCollectionModalOpen && ( - setRunCollectionModalOpen(false)} /> + setRunCollectionModalOpen(false)} /> )} {generateCodeItemModalOpen && ( - setGenerateCodeItemModalOpen(false)} /> + setGenerateCodeItemModalOpen(false)} /> )} {itemInfoModalOpen && ( - setItemInfoModalOpen(false)} /> + setItemInfoModalOpen(false)} /> )} -
+
{ + ref.current = node; + drag(drop(node)); + }} + >
{indents && indents.length - ? indents.map((i) => { - return ( -
-  {/* Indent */} -
- ); - }) + ? indents.map((i) => ( +
+  {/* Indent */} +
+ )) : null}
{ /> ) : null}
- -
+
{item.name} @@ -429,17 +480,16 @@ const CollectionItem = ({ item, collection, searchText }) => {
- {!itemIsCollapsed ? (
{folderItems && folderItems.length ? folderItems.map((i) => { - return ; + return ; }) : null} {requestItems && requestItems.length ? requestItems.map((i) => { - return ; + return ; }) : null}
@@ -448,4 +498,4 @@ const CollectionItem = ({ item, collection, searchText }) => { ); }; -export default CollectionItem; \ No newline at end of file +export default React.memo(CollectionItem); \ No newline at end of file diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/index.js index 9cba09179..17b6dc007 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/index.js @@ -1,12 +1,14 @@ import React from 'react'; import toast from 'react-hot-toast'; import Modal from 'components/Modal'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { IconFiles } from '@tabler/icons'; import { removeCollection } from 'providers/ReduxStore/slices/collections/actions'; +import { findCollectionByUid } from 'utils/collections/index'; -const RemoveCollection = ({ onClose, collection }) => { +const RemoveCollection = ({ onClose, collectionUid }) => { const dispatch = useDispatch(); + const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid)); const onConfirm = () => { dispatch(removeCollection(collection.uid)) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/RenameCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/RenameCollection/index.js index a6e11051e..0d3a4c34a 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/RenameCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/RenameCollection/index.js @@ -2,13 +2,15 @@ import React, { useRef, useEffect } from 'react'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import Modal from 'components/Modal'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import toast from 'react-hot-toast'; import { renameCollection } from 'providers/ReduxStore/slices/collections/actions'; +import { findCollectionByUid } from 'utils/collections/index'; -const RenameCollection = ({ collection, onClose }) => { +const RenameCollection = ({ collectionUid, onClose }) => { const dispatch = useDispatch(); const inputRef = useRef(); + const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid)); const formik = useFormik({ enableReinitialize: true, initialValues: { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js index 5c06cc42a..b47881fad 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js @@ -13,7 +13,8 @@ const Wrapper = styled.div` } &.item-hovered { - background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + border-top: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border}; + border-bottom: 2px solid transparent; .collection-actions { .dropdown { div[aria-expanded='false'] { @@ -62,6 +63,36 @@ const Wrapper = styled.div` color: white; } } + + &.drop-target { + background-color: ${(props) => props.theme.dragAndDrop.hoverBg}; + transition: ${(props) => props.theme.dragAndDrop.transition}; + } + + &.drop-target-above { + border: none; + border-top: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border}; + margin-top: -2px; + background: transparent; + transition: ${(props) => props.theme.dragAndDrop.transition}; + } + + &.drop-target-below { + border: none; + border-bottom: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border}; + margin-bottom: -2px; + background: transparent; + transition: ${(props) => props.theme.dragAndDrop.transition}; + } + } + + .collection-name.drop-target { + border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border}; + border-radius: 4px; + background-color: ${(props) => props.theme.dragAndDrop.hoverBg}; + margin: -2px; + transition: ${(props) => props.theme.dragAndDrop.transition}; + box-shadow: 0 0 0 2px ${(props) => props.theme.dragAndDrop.hoverBg}; } #sidebar-collection-name { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index e81b43a1f..16fa52b21 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -1,4 +1,5 @@ import React, { useState, forwardRef, useRef, useEffect } from 'react'; +import { getEmptyImage } from 'react-dnd-html5-backend'; import classnames from 'classnames'; import { uuid } from 'utils/common'; import filter from 'lodash/filter'; @@ -6,8 +7,8 @@ import { useDrop, useDrag } 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 { mountCollection, moveCollectionAndPersist, handleCollectionItemDrop } from 'providers/ReduxStore/slices/collections/actions'; +import { useDispatch } from 'react-redux'; import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; import NewRequest from 'components/Sidebar/NewRequest'; import NewFolder from 'components/Sidebar/NewFolder'; @@ -19,9 +20,10 @@ 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 { areItemsLoading } from 'utils/collections'; import { scrollToTheActiveTab } from 'utils/tabs'; import ShareCollection from 'components/ShareCollection/index'; +import { CollectionItemDragPreview } from './CollectionItem/CollectionItemDragPreview/index'; const Collection = ({ collection, searchText }) => { const [showNewFolderModal, setShowNewFolderModal] = useState(false); @@ -33,7 +35,7 @@ const Collection = ({ collection, searchText }) => { const dispatch = useDispatch(); const isLoading = areItemsLoading(collection); const collectionRef = useRef(null); - + const menuDropdownTippyRef = useRef(); const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref); const MenuIcon = forwardRef((props, ref) => { @@ -127,8 +129,8 @@ const Collection = ({ collection, searchText }) => { const isCollectionItem = (itemType) => { return itemType.startsWith('collection-item'); }; - - const [{ isDragging }, drag] = useDrag({ + + const [{ isDragging }, drag, dragPreview] = useDrag({ type: "collection", item: collection, collect: (monitor) => ({ @@ -144,7 +146,7 @@ const Collection = ({ collection, searchText }) => { drop: (draggedItem, monitor) => { const itemType = monitor.getItemType(); if (isCollectionItem(itemType)) { - dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid)) + dispatch(handleCollectionItemDrop({ targetItem: collection, draggedItem, dropType: 'inside', collectionUid: collection.uid })) } else { dispatch(moveCollectionAndPersist({draggedItem, targetItem: collection})); } @@ -157,7 +159,9 @@ const Collection = ({ collection, searchText }) => { }), }); - drag(drop(collectionRef)); + useEffect(() => { + dragPreview(getEmptyImage(), { captureDraggingState: true }); + }, []); if (searchText && searchText.length) { if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) { @@ -170,36 +174,35 @@ const Collection = ({ collection, searchText }) => { }); // we need to sort request items by seq property - const sortRequestItems = (items = []) => { + const sortItemsBySequence = (items = []) => { return items.sort((a, b) => a.seq - b.seq); }; - // we need to sort folder items by name alphabetically - const sortFolderItems = (items = []) => { - return items.sort((a, b) => a.name.localeCompare(b.name)); - }; - - const requestItems = sortRequestItems(filter(collection.items, (i) => isItemARequest(i))); - const folderItems = sortFolderItems(filter(collection.items, (i) => isItemAFolder(i))); + const requestItems = sortItemsBySequence(filter(collection.items, (i) => isItemARequest(i))); + const folderItems = sortItemsBySequence(filter(collection.items, (i) => isItemAFolder(i))); return ( - {showNewRequestModal && setShowNewRequestModal(false)} />} - {showNewFolderModal && setShowNewFolderModal(false)} />} + {showNewRequestModal && setShowNewRequestModal(false)} />} + {showNewFolderModal && setShowNewFolderModal(false)} />} {showRenameCollectionModal && ( - setShowRenameCollectionModal(false)} /> + setShowRenameCollectionModal(false)} /> )} {showRemoveCollectionModal && ( - setShowRemoveCollectionModal(false)} /> + setShowRemoveCollectionModal(false)} /> )} {showShareCollectionModal && ( - setShowShareCollectionModal(false)} /> + setShowShareCollectionModal(false)} /> )} {showCloneCollectionModalOpen && ( - setShowCloneCollectionModalOpen(false)} /> + setShowCloneCollectionModalOpen(false)} /> )} +
{ + collectionRef.current = node; + drag(drop(node)); + }} >
{
-
{!collectionIsCollapsed ? (
- {folderItems && folderItems.length - ? folderItems.map((i) => { - return ; - }) - : null} - {requestItems && requestItems.length - ? requestItems.map((i) => { - return ; - }) - : null} + {folderItems?.map?.((i) => { + return ; + })} + {requestItems?.map?.((i) => { + return ; + })}
) : null}
diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js index 3b3c6aeaa..0b71125c8 100644 --- a/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js @@ -1,34 +1,43 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; +import { IconLoader2 } from '@tabler/icons'; import importBrunoCollection from 'utils/importers/bruno-collection'; -import importPostmanCollection from 'utils/importers/postman-collection'; +import { postmanToBruno, readFile } from 'utils/importers/postman-collection'; import importInsomniaCollection from 'utils/importers/insomnia-collection'; import importOpenapiCollection from 'utils/importers/openapi-collection'; import { toastError } from 'utils/common/error'; import Modal from 'components/Modal'; +import fileDialog from 'file-dialog'; const ImportCollection = ({ onClose, handleSubmit }) => { + const [isLoading, setIsLoading] = useState(false) + const handleImportBrunoCollection = () => { importBrunoCollection() .then(({ collection }) => { handleSubmit({ collection }); }) - .catch((err) => toastError(err, 'Import collection failed')); + .catch((err) => toastError(err, 'Import collection failed')) }; + const handleImportPostmanCollection = () => { - importPostmanCollection() - .then(({ collection }) => { - handleSubmit({ collection }); + fileDialog({ accept: 'application/json' }) + .then((...args) => { + setIsLoading(true); + return readFile(...args); }) - .catch((err) => toastError(err, 'Postman Import collection failed')); - }; + .then((collection) => postmanToBruno(collection)) + .then((collection) => handleSubmit({ collection })) + .catch((err) => toastError(err, 'Postman Import collection failed')) + .finally(() => setIsLoading(false)); + } const handleImportInsomniaCollection = () => { importInsomniaCollection() .then(({ collection }) => { handleSubmit({ collection }); }) - .catch((err) => toastError(err, 'Insomnia Import collection failed')); + .catch((err) => toastError(err, 'Insomnia Import collection failed')) }; const handleImportOpenapiCollection = () => { @@ -36,8 +45,9 @@ const ImportCollection = ({ onClose, handleSubmit }) => { .then(({ collection }) => { handleSubmit({ collection }); }) - .catch((err) => toastError(err, 'OpenAPI v3 Import collection failed')); + .catch((err) => toastError(err, 'OpenAPI v3 Import collection failed')) }; + const CollectionButton = ({ children, className, onClick }) => { return ( ); }; - return ( - -
-

Select the type of your existing collection :

-
- Bruno Collection - Postman Collection - Insomnia Collection - OpenAPI V3 Spec + + const FullscreenLoader = () => { + const [loadingMessage, setLoadingMessage] = useState(''); + + // Messages to cycle through while loading + const loadingMessages = [ + 'Processing collection...', + 'Analyzing requests...', + 'Translating scripts...', + 'Preparing collection...', + 'Almost done...' + ]; + + + // Cycle through loading messages for better UX + useEffect(() => { + if (!isLoading) return; + + let messageIndex = 0; + const interval = setInterval(() => { + messageIndex = (messageIndex + 1) % loadingMessages.length; + setLoadingMessage(loadingMessages[messageIndex]); + }, 2000); + + setLoadingMessage(loadingMessages[0]); + + return () => clearInterval(interval); + }, [isLoading]); + + return ( +
+
+ +

+ {loadingMessage} +

+

+ This may take a moment depending on the collection size +

- + ); + }; + + return ( + <> + {isLoading && } + {!isLoading && ( + +
+

Select the type of your existing collection :

+
+ Bruno Collection + Postman Collection + Insomnia Collection + OpenAPI V3 Spec +
+
+
+ )} + ); }; diff --git a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js index c0b39b727..83c243653 100644 --- a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js +++ b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js @@ -14,7 +14,7 @@ import Dropdown from "components/Dropdown"; import { IconCaretDown } from "@tabler/icons"; import StyledWrapper from './StyledWrapper'; -const NewFolder = ({ collection, item, onClose }) => { +const NewFolder = ({ collectionUid, item, onClose }) => { const dispatch = useDispatch(); const inputRef = useRef(); const [isEditing, toggleEditing] = useState(false); @@ -52,7 +52,7 @@ const NewFolder = ({ collection, item, onClose }) => { }) }), onSubmit: (values) => { - dispatch(newFolder(values.folderName, values.directoryName, collection.uid, item ? item.uid : null)) + dispatch(newFolder(values.folderName, values.directoryName, collectionUid, item ? item.uid : null)) .then(() => { toast.success('New folder created!'); onClose(); diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js index 61fdcd22a..ec8f5dfda 100644 --- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js +++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js @@ -5,7 +5,7 @@ import toast from 'react-hot-toast'; import path from 'utils/common/path'; import { uuid } from 'utils/common'; import Modal from 'components/Modal'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { newEphemeralHttpRequest } from 'providers/ReduxStore/slices/collections'; import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions'; import { addTab } from 'providers/ReduxStore/slices/tabs'; @@ -20,9 +20,11 @@ import Portal from 'components/Portal'; import Help from 'components/Help'; import StyledWrapper from './StyledWrapper'; -const NewRequest = ({ collection, item, isEphemeral, onClose }) => { +const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => { const dispatch = useDispatch(); const inputRef = useRef(); + + const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid)); const { brunoConfig: { presets: collectionPresets = {} } } = collection; @@ -135,14 +137,14 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { requestType: values.requestType, requestUrl: values.requestUrl, requestMethod: values.requestMethod, - collectionUid: collection.uid + collectionUid: collectionUid }) ) .then(() => { dispatch( addTab({ uid: uid, - collectionUid: collection.uid, + collectionUid: collectionUid, requestPaneTab: getDefaultRequestPaneTab({ type: values.requestType }) }) ); @@ -158,7 +160,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { requestType: curlRequestTypeDetected, requestUrl: request.url, requestMethod: request.method, - collectionUid: collection.uid, + collectionUid: collectionUid, itemUid: item ? item.uid : null, headers: request.headers, body: request.body, @@ -178,7 +180,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { requestType: values.requestType, requestUrl: values.requestUrl, requestMethod: values.requestMethod, - collectionUid: collection.uid, + collectionUid: collectionUid, itemUid: item ? item.uid : null }) ) @@ -389,8 +391,6 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { ) : (
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index f6ac62bc9..6aa890d8b 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -13,12 +13,9 @@ import { findEnvironmentInCollection, findItemInCollection, findParentItemInCollection, - getItemsToResequence, isItemAFolder, refreshUidsInItem, isItemARequest, - moveCollectionItem, - moveCollectionItemToRootOfCollection, transformRequestToSaveToFilesystem } from 'utils/collections'; import { uuid, waitForNextTick } from 'utils/common'; @@ -47,8 +44,7 @@ import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs'; import { resolveRequestFilename } from 'utils/common/platform'; import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index'; import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index'; -import { getGlobalEnvironmentVariables } from 'utils/collections/index'; -import { findCollectionByPathname, findEnvironmentInCollectionByName } from 'utils/collections/index'; +import { getGlobalEnvironmentVariables, findCollectionByPathname, findEnvironmentInCollectionByName, getReorderedItemsInTargetDirectory, resetSequencesInFolder, getReorderedItemsInSourceDirectory, calculateDraggedItemNewPathname } from 'utils/collections/index'; import { sanitizeName } from 'utils/common/regex'; import { safeParseJSON, safeStringifyJSON } from 'utils/common/index'; @@ -60,7 +56,7 @@ export const renameCollection = (newName, collectionUid) => (dispatch, getState) if (!collection) { return reject(new Error('Collection not found')); } - + const { ipcRenderer } = window; ipcRenderer.invoke('renderer:rename-collection', newName, collection.pathname).then(resolve).catch(reject); }); }; @@ -337,6 +333,7 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive, delay) }) ); + const { ipcRenderer } = window; ipcRenderer .invoke( 'renderer:run-collection-folder', @@ -358,6 +355,8 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive, delay) export const newFolder = (folderName, directoryName, collectionUid, itemUid) => (dispatch, getState) => { const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); + const parentItem = itemUid ? findItemInCollection(collection, itemUid) : collection; + const items = filter(parentItem.items, (i) => isItemAFolder(i) || isItemARequest(i)); return new Promise((resolve, reject) => { if (!collection) { @@ -372,10 +371,27 @@ export const newFolder = (folderName, directoryName, collectionUid, itemUid) => if (!folderWithSameNameExists) { const fullName = path.join(collection.pathname, directoryName); const { ipcRenderer } = window; - ipcRenderer - .invoke('renderer:new-folder', fullName, folderName) - .then(() => resolve()) + .invoke('renderer:new-folder', fullName) + .then(async () => { + const folderData = { + name: folderName, + pathname: fullName, + root: { + meta: { + name: folderName, + seq: items?.length + 1 + } + } + }; + ipcRenderer + .invoke('renderer:save-folder-root', folderData) + .then(resolve) + .catch((err) => { + toast.error('Failed to save folder settings!'); + reject(err); + }); + }) .catch((error) => reject(error)); } else { return reject(new Error('Duplicate folder names under same parent folder are not allowed')); @@ -392,8 +408,26 @@ export const newFolder = (folderName, directoryName, collectionUid, itemUid) => const { ipcRenderer } = window; ipcRenderer - .invoke('renderer:new-folder', fullName, folderName) - .then(() => resolve()) + .invoke('renderer:new-folder', fullName) + .then(async () => { + const folderData = { + name: folderName, + pathname: fullName, + root: { + meta: { + name: folderName, + seq: items?.length + 1 + } + } + }; + ipcRenderer + .invoke('renderer:save-folder-root', folderData) + .then(resolve) + .catch((err) => { + toast.error('Failed to save folder settings!'); + reject(err); + }); + }) .catch((error) => reject(error)); } else { return reject(new Error('Duplicate folder names under same parent folder are not allowed')); @@ -495,8 +529,11 @@ export const cloneItem = (newName, newFilename, itemUid, collectionUid) => (disp set(item, 'name', newName); set(item, 'filename', newFilename); set(item, 'root.meta.name', newName); - + set(item, 'root.meta.seq', parentFolder?.items?.length + 1); + const collectionPath = path.join(parentFolder.pathname, newFilename); + + const { ipcRenderer } = window; ipcRenderer.invoke('renderer:clone-folder', item, collectionPath).then(resolve).catch(reject); return; } @@ -594,176 +631,114 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => { export const sortCollections = (payload) => (dispatch) => { dispatch(_sortCollections(payload)); }; -export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => { + +export const moveItem = ({ targetDirname, sourcePathname }) => (dispatch, getState) => { + return new Promise((resolve, reject) => { + const { ipcRenderer } = window; + + ipcRenderer.invoke('renderer:move-item', { targetDirname, sourcePathname }) + .then(resolve) + .catch(reject); + }); +} + +export const handleCollectionItemDrop = ({ targetItem, draggedItem, dropType, collectionUid }) => (dispatch, getState) => { const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); + const { uid: draggedItemUid, pathname: draggedItemPathname } = draggedItem; + const { uid: targetItemUid, pathname: targetItemPathname } = targetItem; + const targetItemDirectory = findParentItemInCollection(collection, targetItemUid) || collection; + const targetItemDirectoryItems = cloneDeep(targetItemDirectory.items); + const draggedItemDirectory = findParentItemInCollection(collection, draggedItemUid) || collection; + const draggedItemDirectoryItems = cloneDeep(draggedItemDirectory.items); + const handleMoveToNewLocation = async ({ draggedItem, draggedItemDirectoryItems, targetItem, targetItemDirectoryItems, newPathname, dropType }) => { + const { uid: targetItemUid } = targetItem; + const { pathname: draggedItemPathname, uid: draggedItemUid } = draggedItem; + + const newDirname = path.dirname(newPathname); + await dispatch(moveItem({ + targetDirname: newDirname, + sourcePathname: draggedItemPathname + })); + + // Update sequences in the source directory + if (draggedItemDirectoryItems?.length) { + // reorder items in the source directory + const draggedItemDirectoryItemsWithoutDraggedItem = draggedItemDirectoryItems.filter(i => i.uid !== draggedItemUid); + const reorderedSourceItems = getReorderedItemsInSourceDirectory({ items: draggedItemDirectoryItemsWithoutDraggedItem }); + if (reorderedSourceItems?.length) { + await dispatch(updateItemsSequences({ itemsToResequence: reorderedSourceItems })); + } + } + + // Update sequences in the target directory (if dropping adjacent) + if (dropType === 'adjacent') { + const targetItemSequence = targetItemDirectoryItems.findIndex(i => i.uid === targetItemUid)?.seq; + + const draggedItemWithNewPathAndSequence = { + ...draggedItem, + pathname: newPathname, + seq: targetItemSequence + }; + + // draggedItem is added to the targetItem's directory + const reorderedTargetItems = getReorderedItemsInTargetDirectory({ + items: [ ...targetItemDirectoryItems, draggedItemWithNewPathAndSequence ], + targetItemUid, + draggedItemUid + }); + + if (reorderedTargetItems?.length) { + await dispatch(updateItemsSequences({ itemsToResequence: reorderedTargetItems })); + } + } + }; + + const handleReorderInSameLocation = async ({ draggedItem, targetItem, targetItemDirectoryItems }) => { + const { uid: targetItemUid } = targetItem; + const { uid: draggedItemUid } = draggedItem; + + // reorder items in the targetItem's directory + const reorderedItems = getReorderedItemsInTargetDirectory({ + items: targetItemDirectoryItems, + targetItemUid, + draggedItemUid + }); + + if (reorderedItems?.length) { + await dispatch(updateItemsSequences({ itemsToResequence: reorderedItems })); + } + }; + + return new Promise(async (resolve, reject) => { + try { + const newPathname = calculateDraggedItemNewPathname({ draggedItem, targetItem, dropType, collectionPathname: collection.pathname }); + if (!newPathname) return; + if (targetItemPathname?.startsWith(draggedItemPathname)) return; + if (newPathname !== draggedItemPathname) { + await handleMoveToNewLocation({ targetItem, targetItemDirectoryItems, draggedItem, draggedItemDirectoryItems, newPathname, dropType }); + } else { + await handleReorderInSameLocation({ draggedItem, targetItemDirectoryItems, targetItem }); + } + resolve(); + } catch (error) { + console.error(error); + toast.error(error?.message); + reject(error); + } + }) +} + +export const updateItemsSequences = ({ itemsToResequence }) => (dispatch, getState) => { return new Promise((resolve, reject) => { - if (!collection) { - return reject(new Error('Collection not found')); - } + const { ipcRenderer } = window; - const collectionCopy = cloneDeep(collection); - const draggedItem = findItemInCollection(collectionCopy, draggedItemUid); - const targetItem = findItemInCollection(collectionCopy, targetItemUid); - - if (!draggedItem) { - return reject(new Error('Dragged item not found')); - } - - if (!targetItem) { - return reject(new Error('Target item not found')); - } - - const draggedItemParent = findParentItemInCollection(collectionCopy, draggedItemUid); - const targetItemParent = findParentItemInCollection(collectionCopy, targetItemUid); - const sameParent = draggedItemParent === targetItemParent; - - // file item dragged onto another file item and both are in the same folder - // this is also true when both items are at the root level - if (isItemARequest(draggedItem) && isItemARequest(targetItem) && sameParent) { - moveCollectionItem(collectionCopy, draggedItem, targetItem); - const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy); - - return ipcRenderer - .invoke('renderer:resequence-items', itemsToResequence) - .then(resolve) - .catch((error) => reject(error)); - } - - // file item dragged onto another file item which is at the root level - if (isItemARequest(draggedItem) && isItemARequest(targetItem) && !targetItemParent) { - const draggedItemPathname = draggedItem.pathname; - moveCollectionItem(collectionCopy, draggedItem, targetItem); - const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy); - const itemsToResequence2 = getItemsToResequence(targetItemParent, collectionCopy); - - return ipcRenderer - .invoke('renderer:move-file-item', draggedItemPathname, collectionCopy.pathname) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence)) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2)) - .then(resolve) - .catch((error) => reject(error)); - } - - // file item dragged onto another file item and both are in different folders - if (isItemARequest(draggedItem) && isItemARequest(targetItem) && !sameParent) { - const draggedItemPathname = draggedItem.pathname; - moveCollectionItem(collectionCopy, draggedItem, targetItem); - const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy); - const itemsToResequence2 = getItemsToResequence(targetItemParent, collectionCopy); - - return ipcRenderer - .invoke('renderer:move-file-item', draggedItemPathname, targetItemParent.pathname) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence)) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2)) - .then(resolve) - .catch((error) => reject(error)); - } - - // file item dragged into its own folder - if (isItemARequest(draggedItem) && isItemAFolder(targetItem) && draggedItemParent === targetItem) { - return resolve(); - } - - // file item dragged into another folder - if (isItemARequest(draggedItem) && isItemAFolder(targetItem) && draggedItemParent !== targetItem) { - const draggedItemPathname = draggedItem.pathname; - moveCollectionItem(collectionCopy, draggedItem, targetItem); - const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy); - const itemsToResequence2 = getItemsToResequence(targetItem, collectionCopy); - - return ipcRenderer - .invoke('renderer:move-file-item', draggedItemPathname, targetItem.pathname) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence)) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2)) - .then(resolve) - .catch((error) => reject(error)); - } - - // end of the file drags, now let's handle folder drags - // folder drags are simpler since we don't allow ordering of folders - - // folder dragged into its own folder - if (isItemAFolder(draggedItem) && isItemAFolder(targetItem) && draggedItemParent === targetItem) { - return resolve(); - } - - // folder dragged into a file which is at the same level - // this is also true when both items are at the root level - if (isItemAFolder(draggedItem) && isItemARequest(targetItem) && sameParent) { - return resolve(); - } - - // folder dragged into a file which is a child of the folder - if (isItemAFolder(draggedItem) && isItemARequest(targetItem) && draggedItem === targetItemParent) { - return resolve(); - } - - // folder dragged into a file which is at the root level - if (isItemAFolder(draggedItem) && isItemARequest(targetItem) && !targetItemParent) { - const draggedItemPathname = draggedItem.pathname; - - return ipcRenderer - .invoke('renderer:move-folder-item', draggedItemPathname, collectionCopy.pathname) - .then(resolve) - .catch((error) => reject(error)); - } - - // folder dragged into another folder - if (isItemAFolder(draggedItem) && isItemAFolder(targetItem) && draggedItemParent !== targetItem) { - const draggedItemPathname = draggedItem.pathname; - - return ipcRenderer - .invoke('renderer:move-folder-item', draggedItemPathname, targetItem.pathname) - .then(resolve) - .catch((error) => reject(error)); - } + ipcRenderer.invoke('renderer:resequence-items', itemsToResequence) + .then(resolve) + .catch(reject); }); -}; - -export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (dispatch, getState) => { - const state = getState(); - const collection = findCollectionByUid(state.collections.collections, collectionUid); - - return new Promise((resolve, reject) => { - if (!collection) { - return reject(new Error('Collection not found')); - } - - const collectionCopy = cloneDeep(collection); - const draggedItem = findItemInCollection(collectionCopy, draggedItemUid); - if (!draggedItem) { - return reject(new Error('Dragged item not found')); - } - - const draggedItemParent = findParentItemInCollection(collectionCopy, draggedItemUid); - // file item is already at the root level - if (!draggedItemParent) { - return resolve(); - } - - const draggedItemPathname = draggedItem.pathname; - moveCollectionItemToRootOfCollection(collectionCopy, draggedItem); - - if (isItemAFolder(draggedItem)) { - return ipcRenderer - .invoke('renderer:move-folder-item', draggedItemPathname, collectionCopy.pathname) - .then(resolve) - .catch((error) => reject(error)); - } else { - const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy); - const itemsToResequence2 = getItemsToResequence(collectionCopy, collectionCopy); - - return ipcRenderer - .invoke('renderer:move-file-item', draggedItemPathname, collectionCopy.pathname) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence)) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2)) - .then(resolve) - .catch((error) => reject(error)); - } - }); -}; +} export const newHttpRequest = (params) => (dispatch, getState) => { const { requestName, filename, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params; @@ -823,8 +798,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => { collection.items, (i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename) ); - const requestItems = filter(collection.items, (i) => i.type !== 'folder'); - item.seq = requestItems.length + 1; + const items = filter(collection.items, (i) => isItemAFolder(i) || isItemARequest(i)); + item.seq = items.length + 1; if (!reqWithSameNameExists) { const fullName = path.join(collection.pathname, resolvedFilename); @@ -852,8 +827,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => { currentItem.items, (i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename) ); - const requestItems = filter(currentItem.items, (i) => i.type !== 'folder'); - item.seq = requestItems.length + 1; + const items = filter(currentItem.items, (i) => isItemAFolder(i) || isItemARequest(i)); + item.seq = items.length + 1; if (!reqWithSameNameExists) { const fullName = path.join(currentItem.pathname, resolvedFilename); const { ipcRenderer } = window; @@ -885,6 +860,7 @@ export const addEnvironment = (name, collectionUid) => (dispatch, getState) => { return reject(new Error('Collection not found')); } + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:create-environment', collection.pathname, name) .then( @@ -913,6 +889,7 @@ export const importEnvironment = (name, variables, collectionUid) => (dispatch, const sanitizedName = sanitizeName(name); + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:create-environment', collection.pathname, sanitizedName, variables) .then( @@ -946,6 +923,7 @@ export const copyEnvironment = (name, baseEnvUid, collectionUid) => (dispatch, g const sanitizedName = sanitizeName(name); + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:create-environment', collection.pathname, sanitizedName, baseEnv.variables) .then( @@ -982,6 +960,7 @@ export const renameEnvironment = (newName, environmentUid, collectionUid) => (di const oldName = environment.name; environment.name = sanitizedName; + const { ipcRenderer } = window; environmentSchema .validate(environment) .then(() => ipcRenderer.invoke('renderer:rename-environment', collection.pathname, oldName, sanitizedName)) @@ -1005,6 +984,7 @@ export const deleteEnvironment = (environmentUid, collectionUid) => (dispatch, g return reject(new Error('Environment not found')); } + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:delete-environment', collection.pathname, environment.name) .then(resolve) @@ -1028,6 +1008,7 @@ export const saveEnvironment = (variables, environmentUid, collectionUid) => (di environment.variables = variables; + const { ipcRenderer } = window; environmentSchema .validate(environment) .then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, environment)) @@ -1053,7 +1034,8 @@ export const selectEnvironment = (environmentUid, collectionUid) => (dispatch, g if (environmentUid && !environmentName) { return reject(new Error('Environment not found')); } - + + const { ipcRenderer } = window; ipcRenderer.invoke('renderer:update-ui-state-snapshot', { type: 'COLLECTION_ENVIRONMENT', data: { collectionPath: collection?.pathname, environmentName }}); dispatch(_selectEnvironment({ environmentUid, collectionUid })); @@ -1112,11 +1094,13 @@ export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getS const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); - if (!collection) { - return reject(new Error('Collection not found')); - } return new Promise((resolve, reject) => { + if (!collection) { + return reject(new Error('Collection not found')); + } + + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:update-bruno-config', brunoConfig, collection.pathname, collectionUid) .then(resolve) @@ -1135,6 +1119,8 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge brunoConfig: brunoConfig }; + const { ipcRenderer } = window; + return new Promise((resolve, reject) => { ipcRenderer.invoke('renderer:get-collection-security-config', pathname).then((securityConfig) => { collectionSchema diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 5e8275ba1..3dfa6d052 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -1719,6 +1719,9 @@ export const collectionsSlice = createSlice({ folderItem.name = file?.data?.meta?.name; } folderItem.root = file.data; + if (file?.data?.meta?.seq) { + folderItem.seq = file.data?.meta?.seq; + } } return; } @@ -1795,9 +1798,10 @@ export const collectionsSlice = createSlice({ currentPath = path.join(currentPath, directoryName); if (!childItem) { childItem = { - uid: uuid(), + uid: dir?.meta?.uid || uuid(), pathname: currentPath, name: dir?.meta?.name || directoryName, + seq: dir?.meta?.seq || 1, filename: directoryName, collapsed: true, type: 'folder', @@ -1829,6 +1833,9 @@ export const collectionsSlice = createSlice({ if (file?.data?.meta?.name) { folderItem.name = file?.data?.meta?.name; } + if (file?.data?.meta?.seq) { + folderItem.seq = file?.data?.meta?.seq; + } folderItem.root = file.data; } return; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js b/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js index def88f2b6..b208f14fd 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js @@ -1,5 +1,5 @@ import { createSlice } from '@reduxjs/toolkit'; -import { stringifyIfNot, uuid } from 'utils/common/index'; +import { uuid } from 'utils/common/index'; import { environmentSchema } from '@usebruno/schema'; import { cloneDeep } from 'lodash'; @@ -90,6 +90,7 @@ export const { export const addGlobalEnvironment = ({ name, variables = [] }) => (dispatch, getState) => { return new Promise((resolve, reject) => { const uid = uuid(); + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:create-global-environment', { name, uid, variables }) .then(() => dispatch(_addGlobalEnvironment({ name, uid, variables }))) @@ -104,6 +105,7 @@ export const copyGlobalEnvironment = ({ name, environmentUid: baseEnvUid }) => ( const globalEnvironments = state.globalEnvironments.globalEnvironments; const baseEnv = globalEnvironments?.find(env => env?.uid == baseEnvUid) const uid = uuid(); + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:create-global-environment', { uid, name, variables: baseEnv.variables }) .then(() => dispatch(_copyGlobalEnvironment({ name, uid, variables: baseEnv.variables }))) @@ -114,6 +116,7 @@ export const copyGlobalEnvironment = ({ name, environmentUid: baseEnvUid }) => ( export const renameGlobalEnvironment = ({ name: newName, environmentUid }) => (dispatch, getState) => { return new Promise((resolve, reject) => { + const { ipcRenderer } = window; const state = getState(); const globalEnvironments = state.globalEnvironments.globalEnvironments; const environment = globalEnvironments?.find(env => env?.uid == environmentUid) @@ -139,6 +142,7 @@ export const saveGlobalEnvironment = ({ variables, environmentUid }) => (dispatc return reject(new Error('Environment not found')); } + const { ipcRenderer } = window; environmentSchema .validate(environment) .then(() => ipcRenderer.invoke('renderer:save-global-environment', { @@ -155,6 +159,7 @@ export const saveGlobalEnvironment = ({ variables, environmentUid }) => (dispatc export const selectGlobalEnvironment = ({ environmentUid }) => (dispatch, getState) => { return new Promise((resolve, reject) => { + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:select-global-environment', { environmentUid }) .then(() => dispatch(_selectGlobalEnvironment({ environmentUid }))) @@ -165,6 +170,7 @@ export const selectGlobalEnvironment = ({ environmentUid }) => (dispatch, getSta export const deleteGlobalEnvironment = ({ environmentUid }) => (dispatch, getState) => { return new Promise((resolve, reject) => { + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:delete-global-environment', { environmentUid }) .then(() => dispatch(_deleteGlobalEnvironment({ environmentUid }))) @@ -175,6 +181,7 @@ export const deleteGlobalEnvironment = ({ environmentUid }) => (dispatch, getSta export const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables }) => (dispatch, getState) => { return new Promise((resolve, reject) => { + const { ipcRenderer } = window; if (!globalEnvironmentVariables) resolve(); const state = getState(); diff --git a/packages/bruno-app/src/selectors/tab.js b/packages/bruno-app/src/selectors/tab.js new file mode 100644 index 000000000..76aa67365 --- /dev/null +++ b/packages/bruno-app/src/selectors/tab.js @@ -0,0 +1,9 @@ +import { createSelector } from '@reduxjs/toolkit'; + +export const isTabForItemActive = ({ itemUid }) => createSelector([ + (state) => state.tabs?.activeTabUid +], (activeTabUid) => activeTabUid === itemUid); + +export const isTabForItemPresent = ({ itemUid }) => createSelector([ + (state) => state.tabs.tabs, +], (tabs) => tabs.some((tab) => tab.uid === itemUid)); \ No newline at end of file diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js index 861290981..04ee6134e 100644 --- a/packages/bruno-app/src/themes/dark.js +++ b/packages/bruno-app/src/themes/dark.js @@ -281,6 +281,12 @@ const darkTheme = { color: 'rgb(52 51 49)' }, + dragAndDrop: { + border: '#666666', + borderStyle: '2px solid', + hoverBg: 'rgba(102, 102, 102, 0.08)', + transition: 'all 0.1s ease' + }, infoTip: { bg: '#1f1f1f', border: '#333333', diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js index 6ce9fa583..e95b0e45e 100644 --- a/packages/bruno-app/src/themes/light.js +++ b/packages/bruno-app/src/themes/light.js @@ -282,6 +282,12 @@ const lightTheme = { color: 'rgb(152 151 149)' }, + dragAndDrop: { + border: '#8b8b8b', // Using the same gray as focusBorder from input + borderStyle: '2px solid', + hoverBg: 'rgba(139, 139, 139, 0.05)', // Matching the border color with reduced opacity + transition: 'all 0.1s ease' + }, infoTip: { bg: 'white', border: '#e0e0e0', diff --git a/packages/bruno-app/src/utils/codemirror/javascript-lint.js b/packages/bruno-app/src/utils/codemirror/javascript-lint.js index 88829322e..0038406aa 100644 --- a/packages/bruno-app/src/utils/codemirror/javascript-lint.js +++ b/packages/bruno-app/src/utils/codemirror/javascript-lint.js @@ -5,6 +5,8 @@ * Copyright (C) 2017 by Marijn Haverbeke and others */ +import { JSHINT } from 'jshint'; + let CodeMirror; const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true; diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index e258c80ba..61ce02f50 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -98,6 +98,14 @@ export const findItemInCollectionByPathname = (collection, pathname) => { return findItemByPathname(flattenedItems, pathname); }; +export const findParentItemInCollectionByPathname = (collection, pathname) => { + let flattenedItems = flattenItems(collection.items); + + return find(flattenedItems, (item) => { + return item.items && find(item.items, (i) => i.pathname === pathname); + }); +}; + export const findItemInCollection = (collection, itemUid) => { let flattenedItems = flattenItems(collection.items); @@ -150,90 +158,6 @@ export const getItemsLoadStats = (folder) => { }; } -export const moveCollectionItem = (collection, draggedItem, targetItem) => { - let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid); - - if (draggedItemParent) { - draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq); - draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid); - draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename); - } else { - collection.items = sortBy(collection.items, (item) => item.seq); - collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid); - } - - if (targetItem.type === 'folder') { - targetItem.items = sortBy(targetItem.items || [], (item) => item.seq); - targetItem.items.push(draggedItem); - draggedItem.pathname = path.join(targetItem.pathname, draggedItem.filename); - } else { - let targetItemParent = findParentItemInCollection(collection, targetItem.uid); - - if (targetItemParent) { - targetItemParent.items = sortBy(targetItemParent.items, (item) => item.seq); - let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid); - targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem); - draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename); - } else { - collection.items = sortBy(collection.items, (item) => item.seq); - let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid); - collection.items.splice(targetItemIndex + 1, 0, draggedItem); - draggedItem.pathname = path.join(collection.pathname, draggedItem.filename); - } - } -}; - -export const moveCollectionItemToRootOfCollection = (collection, draggedItem) => { - let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid); - - // If the dragged item is already at the root of the collection, do nothing - if (!draggedItemParent) { - return; - } - - draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq); - draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid); - collection.items = sortBy(collection.items, (item) => item.seq); - collection.items.push(draggedItem); - if (draggedItem.type == 'folder') { - draggedItem.pathname = path.join(collection.pathname, draggedItem.name); - } else { - draggedItem.pathname = path.join(collection.pathname, draggedItem.filename); - } -}; - -export const getItemsToResequence = (parent, collection) => { - let itemsToResequence = []; - - if (!parent) { - let index = 1; - each(collection.items, (item) => { - if (isItemARequest(item)) { - itemsToResequence.push({ - pathname: item.pathname, - seq: index++ - }); - } - }); - return itemsToResequence; - } - - if (parent.items && parent.items.length) { - let index = 1; - each(parent.items, (item) => { - if (isItemARequest(item)) { - itemsToResequence.push({ - pathname: item.pathname, - seq: index++ - }); - } - }); - return itemsToResequence; - } - - return itemsToResequence; -}; - export const transformCollectionToSaveToExportAsFile = (collection, options = {}) => { const copyHeaders = (headers) => { return map(headers, (header) => { @@ -502,6 +426,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} if (meta?.name) { di.root.meta = {}; di.root.meta.name = meta?.name; + di.root.meta.seq = meta?.seq; } if (!Object.keys(di.root.request)?.length) { delete di.root.request; @@ -1086,3 +1011,77 @@ export const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] }); return credentialsVariables; }; + + +// item sequence utils - START + +export const resetSequencesInFolder = (folderItems) => { + const items = folderItems; + const sortedItems = items.sort((a, b) => a.seq - b.seq); + return sortedItems.map((item, index) => { + item.seq = index + 1; + return item; + }); +}; + +export const isItemBetweenSequences = (itemSequence, sourceItemSequence, targetItemSequence) => { + if (targetItemSequence > sourceItemSequence) { + return itemSequence > sourceItemSequence && itemSequence < targetItemSequence; + } + return itemSequence < sourceItemSequence && itemSequence >= targetItemSequence; +}; + +export const calculateNewSequence = (isDraggedItem, targetSequence, draggedSequence) => { + if (!isDraggedItem) { + return null; + } + return targetSequence > draggedSequence ? targetSequence - 1 : targetSequence; +}; + +export const getReorderedItemsInTargetDirectory = ({ items, targetItemUid, draggedItemUid }) => { + const itemsWithFixedSequences = resetSequencesInFolder(cloneDeep(items)); + const targetItem = findItem(itemsWithFixedSequences, targetItemUid); + const draggedItem = findItem(itemsWithFixedSequences, draggedItemUid); + const targetSequence = targetItem?.seq; + const draggedSequence = draggedItem?.seq; + itemsWithFixedSequences?.forEach(item => { + const isDraggedItem = item?.uid === draggedItemUid; + const isBetween = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence); + if (isBetween) { + item.seq += targetSequence > draggedSequence ? -1 : 1; + } + const newSequence = calculateNewSequence(isDraggedItem, targetSequence, draggedSequence); + if (newSequence !== null) { + item.seq = newSequence; + } + }); + // only return items that have been reordered + return itemsWithFixedSequences.filter(item => + items?.find(originalItem => originalItem?.uid === item?.uid)?.seq !== item?.seq + ); +}; + +export const getReorderedItemsInSourceDirectory = ({ items }) => { + const itemsWithFixedSequences = resetSequencesInFolder(cloneDeep(items)); + return itemsWithFixedSequences.filter(item => + items?.find(originalItem => originalItem?.uid === item?.uid)?.seq !== item?.seq + ); +}; + +export const calculateDraggedItemNewPathname = ({ draggedItem, targetItem, dropType, collectionPathname }) => { + const { pathname: targetItemPathname } = targetItem; + const { filename: draggedItemFilename } = draggedItem; + const targetItemDirname = path.dirname(targetItemPathname); + const isTargetTheCollection = targetItemPathname === collectionPathname; + const isTargetItemAFolder = isItemAFolder(targetItem); + + if (dropType === 'inside' && (isTargetItemAFolder || isTargetTheCollection)) { + return path.join(targetItemPathname, draggedItemFilename) + } else if (dropType === 'adjacent') { + return path.join(targetItemDirname, draggedItemFilename) + } + return null; +}; + +// item sequence utils - END + diff --git a/packages/bruno-app/src/utils/common/cache.js b/packages/bruno-app/src/utils/common/cache.js deleted file mode 100644 index d8cee9e50..000000000 --- a/packages/bruno-app/src/utils/common/cache.js +++ /dev/null @@ -1,10 +0,0 @@ -class Cache { - get(key) { - return window.localStorage.getItem(key); - } - set(key, val) { - window.localStorage.setItem(key, val); - } -} - -module.exports = new Cache(); diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js index 8bafbb8f9..47055cbb7 100644 --- a/packages/bruno-app/src/utils/common/index.js +++ b/packages/bruno-app/src/utils/common/index.js @@ -53,7 +53,7 @@ export const safeStringifyJSON = (obj, indent = false) => { export const convertToCodeMirrorJson = (obj) => { try { - return JSON5.stringify(obj).slice(1, -1); + return JSON.stringify(obj, null, 2).slice(1, -1); } catch (e) { return obj; } diff --git a/packages/bruno-app/src/utils/common/regex.js b/packages/bruno-app/src/utils/common/regex.js index 6bf5d7b4d..a8e4c6eee 100644 --- a/packages/bruno-app/src/utils/common/regex.js +++ b/packages/bruno-app/src/utils/common/regex.js @@ -1,16 +1,16 @@ const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g; // replace invalid characters with hyphens const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i; -const firstCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot, space, or hyphen at start -const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no invalid characters -const lastCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot or space at end, hyphen allowed +const firstCharacter = /^[^\s\-<>:"/\\|?*\x00-\x1F]/; // no space, hyphen and `invalidCharacters` +const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no `invalidCharacters` +const lastCharacter = /[^.\s<>:"/\\|?*\x00-\x1F]$/; // no dot, space and `invalidCharacters` export const variableNameRegex = /^[\w-.]*$/; export const sanitizeName = (name) => { name = name - .replace(invalidCharacters, '-') // replace invalid characters with hyphens - .replace(/^[.\s-]+/, '') // remove leading dots, hyphens and spaces - .replace(/[.\s]+$/, ''); // remove trailing dots and spaces (keep trailing hyphens) + .replace(invalidCharacters, '-') // replace invalid characters with hyphens + .replace(/^[\s\-]+/, '') // remove leading spaces and hyphens + .replace(/[.\s]+$/, ''); // remove trailing dots and spaces return name; }; diff --git a/packages/bruno-app/src/utils/common/regex.spec.js b/packages/bruno-app/src/utils/common/regex.spec.js index e7a8b8d36..3994a2b2d 100644 --- a/packages/bruno-app/src/utils/common/regex.spec.js +++ b/packages/bruno-app/src/utils/common/regex.spec.js @@ -23,8 +23,8 @@ describe('regex validators', () => { }); it('should remove trailing periods', () => { - expect(sanitizeName('.file')).toBe('file'); - expect(sanitizeName('.file.')).toBe('file'); + expect(sanitizeName('.file')).toBe('.file'); + expect(sanitizeName('.file.')).toBe('.file'); expect(sanitizeName('file.')).toBe('file'); expect(sanitizeName('file.name.')).toBe('file.name'); expect(sanitizeName('hello world.')).toBe('hello world'); @@ -83,11 +83,11 @@ describe('regex validators', () => { it('should handle filenames with multiple consecutive periods (only remove trailing)', () => { expect(sanitizeName('file.name...')).toBe('file.name'); - expect(sanitizeName('...file')).toBe('file'); + expect(sanitizeName('...file')).toBe('...file'); expect(sanitizeName('file.name... ')).toBe('file.name'); - expect(sanitizeName(' ...file')).toBe('file'); - expect(sanitizeName(' ...file ')).toBe('file'); - expect(sanitizeName(' ...file.... ')).toBe('file'); + expect(sanitizeName(' ...file')).toBe('...file'); + expect(sanitizeName(' ...file ')).toBe('...file'); + expect(sanitizeName(' ...file.... ')).toBe('...file'); }); it('should handle very long filenames', () => { diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js index 75db9aaee..b9cceed65 100644 --- a/packages/bruno-app/src/utils/importers/postman-collection.js +++ b/packages/bruno-app/src/utils/importers/postman-collection.js @@ -1,6 +1,5 @@ import fileDialog from 'file-dialog'; import { BrunoError } from 'utils/common/error'; -import { postmanToBruno } from '@usebruno/converters'; import { safeParseJSON } from 'utils/common/index'; const readFile = (files) => { @@ -12,18 +11,15 @@ const readFile = (files) => { }); }; - -const importCollection = () => { +const postmanToBruno = (collection) => { return new Promise((resolve, reject) => { - fileDialog({ accept: 'application/json' }) - .then(readFile) - .then((collection) => postmanToBruno(collection)) - .then((collection) => resolve({ collection })) - .catch((err) => { - console.log(err); - reject(new BrunoError('Import collection failed')); - }) + window.ipcRenderer.invoke('renderer:convert-postman-to-bruno', collection) + .then(result => resolve(result)) + .catch(err => { + console.error('Error converting Postman to Bruno via Electron:', err); + reject(new BrunoError('Conversion failed')); + }); }); }; -export default importCollection; +export { postmanToBruno, readFile }; diff --git a/packages/bruno-app/src/utils/tests/collections/items-sequencing.spec.js b/packages/bruno-app/src/utils/tests/collections/items-sequencing.spec.js new file mode 100644 index 000000000..adfb5dab9 --- /dev/null +++ b/packages/bruno-app/src/utils/tests/collections/items-sequencing.spec.js @@ -0,0 +1,126 @@ +import { resetSequencesInFolder, isItemBetweenSequences } from 'utils/collections/index'; + +describe('resetSequencesInFolder', () => { + it('should fix the sequences in the folder 1', () => { + const folder = { + items: [ + { uid: '1', seq: 1 }, + { uid: '2', seq: 3 }, + { uid: '3', seq: 6 }, + ], + }; + + const fixedFolder = resetSequencesInFolder(folder.items); + expect(fixedFolder).toEqual([ + { uid: '1', seq: 1 }, + { uid: '2', seq: 2 }, + { uid: '3', seq: 3 }, + ]); + }); + + + it('should fix the sequences in the folder 2', () => { + const folder = { + items: [ + { uid: '1', seq: 3 }, + { uid: '2', seq: 1 }, + { uid: '3', seq: 2 }, + ], + }; + + const fixedFolder = resetSequencesInFolder(folder.items); + expect(fixedFolder).toEqual([ + { uid: '2', seq: 1 }, + { uid: '3', seq: 2 }, + { uid: '1', seq: 3 }, + ]); + }); + + it('should fix the sequences in the folder with missing sequences', () => { + const folder = { + items: [ + { uid: '1', seq: 1 }, + { uid: '2', type: 'folder' }, + { uid: '3', type: 'folder' }, + { uid: '4', seq: 7 }, + ] + }; + + const fixedFolder = resetSequencesInFolder(folder.items); + expect(fixedFolder).toEqual([ + { uid: '1', seq: 1 }, + { uid: '2', seq: 2, type: 'folder' }, + { uid: '3', seq: 3, type: 'folder' }, + { uid: '4', seq: 4 }, + ]); + }); + + it('should fix the sequences in the folder with same sequences', () => { + const folder = { + items: [ + { uid: '1', seq: 2 }, + { uid: '2', seq: 2 }, + { uid: '3', seq: 3 }, + { uid: '4', seq: 1 }, + ], + }; + + const fixedFolder = resetSequencesInFolder(folder.items); + expect(fixedFolder).toEqual([ + { uid: '4', seq: 1 }, + { uid: '1', seq: 2 }, + { uid: '2', seq: 3 }, + { uid: '3', seq: 4 }, + ]); + }); +}); + +describe('isItemBetweenSequences', () => { + it('should return true if the item is between the sequences 1', () => { + const item = { uid: '1', seq: 2 }; + const draggedSequence = 1; + const targetSequence = 5; + const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence); + expect(result).toBe(true); + }); + + it('should return true if the item is between the sequences 2', () => { + const item = { uid: '1', seq: 2 }; + const draggedSequence = 1; + const targetSequence = 5; + const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence); + expect(result).toBe(true); + }); + + it('should return true if the item is between the sequences 3', () => { + const item = { uid: '1', seq: 4 }; + const draggedSequence = 1; + const targetSequence = 5; + const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence); + expect(result).toBe(true); + }); + + it('should return true if the item is between the sequences 4', () => { + const item = { uid: '1', seq: 1 }; + const draggedSequence = 5; + const targetSequence = 1; + const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence); + expect(result).toBe(true); + }); + + it('should return false if the item is between the sequences 1', () => { + const item = { uid: '1', seq: 1 }; + const draggedSequence = 1; + const targetSequence = 5; + const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence); + expect(result).toBe(false); + }); + + it('should return false if the item is between the sequences 2', () => { + const item = { uid: '1', seq: 5 }; + const draggedSequence = 1; + const targetSequence = 5; + const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence); + expect(result).toBe(false); + }); +}); diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json index 7347f78fb..e1b74e191 100644 --- a/packages/bruno-cli/package.json +++ b/packages/bruno-cli/package.json @@ -52,6 +52,7 @@ "@usebruno/lang": "0.12.0", "@usebruno/vm2": "^3.9.13", "@usebruno/requests": "^0.1.0", + "@usebruno/converters": "^0.1.0", "aws4-axios": "^3.3.0", "axios": "^1.8.3", "axios-ntlm": "^1.4.2", @@ -63,6 +64,7 @@ "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "iconv-lite": "^0.6.3", + "js-yaml": "^4.1.0", "lodash": "^4.17.21", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", diff --git a/packages/bruno-cli/readme.md b/packages/bruno-cli/readme.md index 616f1b385..c1d38f457 100644 --- a/packages/bruno-cli/readme.md +++ b/packages/bruno-cli/readme.md @@ -58,6 +58,44 @@ If you need to limit the trusted CA to a specified set when validating the reque bru run request.bru --cacert myCustomCA.pem --ignore-truststore ``` +## Importing Collections + +You can import collections from other formats, such as OpenAPI, using the import command: + +```bash +bru import openapi --source api.yml --output ~/Desktop/my-collection --collection-name "My API" +``` + +You can also use the shorter form with aliases: + +```bash +bru import openapi -s api.yml -o ~/Desktop/my-collection -n "My API" +``` + +This creates a Bruno collection directory that can be opened in Bruno. + +You can also import directly from a URL: + +```bash +bru import openapi --source https://example.com/api-spec.json --output ~/Desktop --collection-name "Remote API" +``` + +You can also export the collection as a JSON file: + +```bash +bru import openapi --source api.yml --output-file ~/Desktop/my-collection.json --collection-name "My API" +``` + +Import Options: + +| Option | Details | +| ------------------------- | -------------------------------------------------- | +| --source, -s | Path to the source file or URL (required) | +| --output, -o | Path to the output directory | +| --output-file, -f | Path to the output JSON file | +| --collection-name, -n | Name for the imported collection | +| --insecure | Skip SSL certificate validation when fetching from URLs | + ## Command Line Options | Option | Details | diff --git a/packages/bruno-cli/src/commands/import.js b/packages/bruno-cli/src/commands/import.js new file mode 100644 index 000000000..dd12a8bc3 --- /dev/null +++ b/packages/bruno-cli/src/commands/import.js @@ -0,0 +1,230 @@ +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); +const jsyaml = require('js-yaml'); +const axios = require('axios'); +const { openApiToBruno } = require('@usebruno/converters'); +const { exists, isDirectory, sanitizeName } = require('../utils/filesystem'); +const { createCollectionFromBrunoObject } = require('../utils/collection'); + +const command = 'import '; +const desc = 'Import a collection from other formats'; + +const builder = (yargs) => { + yargs + .positional('type', { + describe: 'Type of collection to import', + type: 'string', + choices: ['openapi'] + }) + .option('source', { + alias: 's', + describe: 'Path to the source file or URL', + type: 'string', + demandOption: true + }) + .option('output', { + alias: 'o', + describe: 'Path to the output directory', + type: 'string', + conflicts: 'output-file' + }) + .option('output-file', { + alias: 'f', + describe: 'Path to the output JSON file', + type: 'string', + conflicts: 'output' + }) + .option('collection-name', { + alias: 'n', + describe: 'Name for the imported collection', + type: 'string' + }) + .option('insecure', { + type: 'boolean', + describe: 'Skip SSL certificate verification when fetching from URLs', + default: false + }) + .example('$0 import openapi --source api.yml --output ~/Desktop/my-collection --collection-name "My API"') + .example('$0 import openapi -s api.yml -o ~/Desktop/my-collection -n "My API"') + .example('$0 import openapi --source https://example.com/api-spec.json --output ~/Desktop --collection-name "Remote API"') + .example('$0 import openapi --source https://self-signed.example.com/api.json --insecure --output ~/Desktop') + .example('$0 import openapi --source api.yml --output-file ~/Desktop/my-collection.json --collection-name "My API"') + .example('$0 import openapi -s api.yml -f ~/Desktop/my-collection.json -n "My API"'); +}; + +const isUrl = (str) => { + try { + return Boolean(new URL(str)); + } catch (error) { + return false; + } +}; + +const readOpenApiFile = async (source, options = {}) => { + try { + let content; + + if (isUrl(source)) { + // Handle URL input + console.log(chalk.yellow(`Fetching specification from URL: ${source}`)); + try { + const axiosOptions = { + timeout: 30000, // 30 second timeout + maxContentLength: 10 * 1024 * 1024, + validateStatus: status => status >= 200 && status < 300 + }; + + // Skip SSL certificate validation if insecure flag is set + if (options.insecure) { + console.log(chalk.yellow('Warning: SSL certificate verification is disabled. Use with caution.')); + axiosOptions.httpsAgent = new (require('https')).Agent({ rejectUnauthorized: false }); + } + + const response = await axios.get(source, axiosOptions); + content = response.data; + } catch (error) { + if (error.code === 'ECONNABORTED') { + throw new Error('Request timed out. The server took too long to respond.'); + } else if (error.code === 'CERT_HAS_EXPIRED' || error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' || + error.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { + throw new Error(`SSL Certificate error: ${error.code}. Try using --insecure if you trust this source.`); + } else if (error.response) { + throw new Error(`Failed to fetch from URL: ${error.response.status} ${error.response.statusText}`); + } else if (error.request) { + throw new Error(`No response received from server. Check the URL and your network connection.`); + } else { + throw new Error(`Error fetching URL: ${error.message}`); + } + } + + // If response is already an object, return it directly + if (typeof content === 'object' && content !== null) { + return content; + } + } else { + // Handle file input + if (!await exists(source)) { + throw new Error(`File does not exist: ${source}`); + } + content = fs.readFileSync(source, 'utf8'); + } + + // If content is a string, try to parse as JSON or YAML + if (typeof content === 'string') { + try { + return JSON.parse(content); + } catch (jsonError) { + try { + return jsyaml.load(content); + } catch (yamlError) { + throw new Error('Failed to parse content as JSON or YAML'); + } + } + } + + return content; + } catch (error) { + // Let the specific error handling from above propagate + throw error; + } +}; + +const handler = async (argv) => { + try { + const { type, source, output, outputFile, collectionName, insecure } = argv; + + if (!type || type !== 'openapi') { + console.error(chalk.red('Only OpenAPI import is supported currently')); + process.exit(1); + } + + if (!source) { + console.error(chalk.red('Source file or URL is required')); + process.exit(1); + } + + if (!output && !outputFile) { + console.error(chalk.red('Either --output or --output-file is required')); + process.exit(1); + } + + console.log(chalk.yellow(`Reading OpenAPI specification from ${source}...`)); + + const openApiSpec = await readOpenApiFile(source, { insecure }); + + if (!openApiSpec) { + console.error(chalk.red('Failed to parse OpenAPI specification')); + process.exit(1); + } + + console.log(chalk.yellow('Converting OpenAPI specification to Bruno format...')); + + // Convert OpenAPI to Bruno format + let brunoCollection = openApiToBruno(openApiSpec); + + // Override collection name if provided + if (collectionName) { + brunoCollection.name = collectionName; + } + + if (outputFile) { + // Save as JSON file + const outputPath = path.resolve(outputFile); + fs.writeFileSync(outputPath, JSON.stringify(brunoCollection, null, 2)); + console.log(chalk.green(`Bruno collection saved as JSON to ${outputPath}`)); + } else if (output) { + const resolvedOutput = path.resolve(output); + + // Check if output is an existing directory + const isOutputDirectory = await exists(resolvedOutput) && isDirectory(resolvedOutput); + + // Determine the final output directory + let outputDir; + if (isOutputDirectory) { + // If output is an existing directory, use collection name to create a subdirectory + const dirName = sanitizeName(brunoCollection.name); + outputDir = path.join(resolvedOutput, dirName); + + // Check if this subfolder already exists + if (await exists(outputDir)) { + const dirContents = fs.readdirSync(outputDir); + if (dirContents.length > 0) { + console.error(chalk.red(`Output directory is not empty: ${outputDir}`)); + process.exit(1); + } + } else { + // Create the subfolder + fs.mkdirSync(outputDir, { recursive: true }); + } + } else { + // If output doesn't exist or is not a directory, use it directly + outputDir = resolvedOutput; + + // Check if parent directory exists + const parentDir = path.dirname(outputDir); + if (!await exists(parentDir)) { + console.error(chalk.red(`Parent directory does not exist: ${parentDir}`)); + process.exit(1); + } + + fs.mkdirSync(outputDir, { recursive: true }); + } + + await createCollectionFromBrunoObject(brunoCollection, outputDir); + console.log(chalk.green(`Bruno collection created at ${outputDir}`)); + } + } catch (error) { + console.error(chalk.red(`Error: ${error.message}`)); + process.exit(1); + } +}; + +module.exports = { + command, + desc, + builder, + handler, + isUrl, + readOpenApiFile +}; \ No newline at end of file diff --git a/packages/bruno-cli/src/utils/collection.js b/packages/bruno-cli/src/utils/collection.js index a6f528389..a300e415f 100644 --- a/packages/bruno-cli/src/utils/collection.js +++ b/packages/bruno-cli/src/utils/collection.js @@ -1,5 +1,9 @@ const { get, each, find, compact } = require('lodash'); const os = require('os'); +const fs = require('fs'); +const path = require('path'); +const { jsonToBruV2, envJsonToBruV2, jsonToCollectionBru } = require('@usebruno/lang'); +const { sanitizeName } = require('./filesystem'); const mergeHeaders = (collection, request, requestTreePath) => { let headers = new Map(); @@ -219,6 +223,136 @@ const getAllRequestsInFolder = (folderItems = [], recursive = true) => { const getAllRequestsAtFolderRoot = (folderItems = []) => { return getAllRequestsInFolder(folderItems, false); +} + +/** + * Safe write file implementation to handle errors + * @param {string} filePath - Path to write file + * @param {string} content - Content to write + */ +const safeWriteFileSync = (filePath, content) => { + try { + fs.writeFileSync(filePath, content, { encoding: 'utf8' }); + } catch (error) { + console.error(`Error writing file ${filePath}:`, error); + } +}; + +/** + * Creates a Bruno collection directory structure from a Bruno collection object + * + * @param {Object} collection - The Bruno collection object + * @param {string} dirPath - The output directory path + */ +const createCollectionFromBrunoObject = async (collection, dirPath) => { + // Create bruno.json + const brunoConfig = { + version: '1', + name: collection.name, + type: 'collection', + ignore: ['node_modules', '.git'] + }; + + fs.writeFileSync( + path.join(dirPath, 'bruno.json'), + JSON.stringify(brunoConfig, null, 2) + ); + + // Create collection.bru if root exists + if (collection.root) { + const collectionContent = await jsonToCollectionBru(collection.root); + fs.writeFileSync(path.join(dirPath, 'collection.bru'), collectionContent); + } + + // Process environments + if (collection.environments && collection.environments.length) { + const envDirPath = path.join(dirPath, 'environments'); + fs.mkdirSync(envDirPath, { recursive: true }); + + for (const env of collection.environments) { + const content = await envJsonToBruV2(env); + const filename = sanitizeName(`${env.name}.bru`); + fs.writeFileSync(path.join(envDirPath, filename), content); + } + } + + // Process collection items + await processCollectionItems(collection.items, dirPath); + + return dirPath; +}; + +/** + * Recursively processes collection items to create files and folders + * + * @param {Array} items - Collection items + * @param {string} currentPath - Current directory path + */ +const processCollectionItems = async (items = [], currentPath) => { + for (const item of items) { + if (item.type === 'folder') { + // Create folder + let sanitizedFolderName = sanitizeName(item?.filename || item?.name); + const folderPath = path.join(currentPath, sanitizedFolderName); + fs.mkdirSync(folderPath, { recursive: true }); + + // Create folder.bru file if root exists + if (item?.root?.meta?.name) { + const folderBruFilePath = path.join(folderPath, 'folder.bru'); + if (item.seq) { + item.root.meta.seq = item.seq; + } + const folderContent = await jsonToCollectionBru( + item.root, + true + ); + safeWriteFileSync(folderBruFilePath, folderContent); + } + + // Process folder items recursively + if (item.items && item.items.length) { + await processCollectionItems(item.items, folderPath); + } + } else if (['http-request', 'graphql-request'].includes(item.type)) { + // Create request file + let sanitizedFilename = sanitizeName(item?.filename || `${item.name}.bru`); + if (!sanitizedFilename.endsWith('.bru')) { + sanitizedFilename += '.bru'; + } + + // Convert JSON to BRU format based on the item type + let type = item.type === 'http-request' ? 'http' : 'graphql'; + const bruJson = { + meta: { + name: item.name, + type: type, + seq: typeof item.seq === 'number' ? item.seq : 1 + }, + http: { + method: (item.request?.method || 'GET').toLowerCase(), + url: item.request?.url || '', + auth: item.request?.auth?.mode || 'none', + body: item.request?.body?.mode || 'none' + }, + params: item.request?.params || [], + headers: item.request?.headers || [], + auth: item.request?.auth || {}, + body: item.request?.body || {}, + script: item.request?.script || {}, + vars: { + req: item.request?.vars?.req || [], + res: item.request?.vars?.res || [] + }, + assertions: item.request?.assertions || [], + tests: item.request?.tests || '', + docs: item.request?.docs || '' + }; + + // Convert to BRU format and write to file + const content = await jsonToBruV2(bruJson); + safeWriteFileSync(path.join(currentPath, sanitizedFilename), content); + } + } }; module.exports = { @@ -228,5 +362,6 @@ module.exports = { findItemInCollection, getTreePathFromCollectionToItem, getAllRequestsInFolder, - getAllRequestsAtFolderRoot + getAllRequestsAtFolderRoot, + createCollectionFromBrunoObject } \ No newline at end of file diff --git a/packages/bruno-cli/src/utils/filesystem.js b/packages/bruno-cli/src/utils/filesystem.js index c3438cebc..46aa6c797 100644 --- a/packages/bruno-cli/src/utils/filesystem.js +++ b/packages/bruno-cli/src/utils/filesystem.js @@ -118,6 +118,46 @@ const getSubDirectories = (dir) => { } }; +/** + * Sanitizes a filename to make it safe for filesystem operations + * + * @param {string} name - The name to sanitize + * @returns {string} - The sanitized name + */ +const sanitizeName = (name) => { + if (!name) return ''; + + const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g; + return name + .replace(invalidCharacters, '-') // replace invalid characters with hyphens + .replace(/^[.\s-]+/, '') // remove leading dots, hyphens and spaces + .replace(/[.\s]+$/, ''); // remove trailing dots and spaces (keep trailing hyphens) +}; + +/** + * Validates if a name is valid for the filesystem + * + * @param {string} name - The name to validate + * @returns {boolean} - True if the name is valid, false otherwise + */ +const validateName = (name) => { + if (!name) return false; + + const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i; + const firstCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot, space, or hyphen at start + const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no invalid characters + const lastCharacter = /[^.\s]$/; // no dot or space at end, hyphen allowed + + if (name.length > 255) return false; // max name length + if (reservedDeviceNames.test(name)) return false; // windows reserved names + + return ( + firstCharacter.test(name) && + middleCharacters.test(name) && + lastCharacter.test(name) + ); +}; + module.exports = { exists, isSymbolicLink, @@ -131,5 +171,7 @@ module.exports = { searchForFiles, searchForBruFiles, stripExtension, - getSubDirectories + getSubDirectories, + sanitizeName, + validateName }; diff --git a/packages/bruno-common/src/runner/reports/html/generate-report.ts b/packages/bruno-common/src/runner/reports/html/generate-report.ts index 378a75197..7309c483e 100644 --- a/packages/bruno-common/src/runner/reports/html/generate-report.ts +++ b/packages/bruno-common/src/runner/reports/html/generate-report.ts @@ -1,6 +1,5 @@ import { T_RunnerResults } from "../../types"; import { isHtmlContentType, getContentType, redactImageData, encodeBase64 } from "../../utils"; -import { getRunnerSummary } from "../../runner-summary"; import htmlTemplateString from "./template"; const generateHtmlReport = ({ @@ -8,7 +7,7 @@ const generateHtmlReport = ({ }: { runnerResults: T_RunnerResults[] }): string => { - const resultsWithSummaryAndCleanData = runnerResults.map(({ iterationIndex, results }) => { + const resultsWithSummaryAndCleanData = runnerResults.map(({ iterationIndex, results, summary }) => { return { iterationIndex, results: results.map((result) => { @@ -29,7 +28,7 @@ const generateHtmlReport = ({ } } }), - summary: getRunnerSummary(results) + summary } }); const htmlString = htmlTemplateString(encodeBase64(JSON.stringify(resultsWithSummaryAndCleanData))); diff --git a/packages/bruno-common/src/runner/types/index.ts b/packages/bruno-common/src/runner/types/index.ts index d9c36e32c..33840f0ad 100644 --- a/packages/bruno-common/src/runner/types/index.ts +++ b/packages/bruno-common/src/runner/types/index.ts @@ -96,6 +96,7 @@ export type T_RunnerResults = { iterationIndex: number; iterationData?: any; // todo - csv/json row data results: T_RunnerRequestExecutionResult[]; + summary: T_RunSummary; } // run summary type diff --git a/packages/bruno-converters/package.json b/packages/bruno-converters/package.json index 3c4051849..b942dea98 100644 --- a/packages/bruno-converters/package.json +++ b/packages/bruno-converters/package.json @@ -21,6 +21,7 @@ "dependencies": { "@usebruno/schema": "^0.7.0", "js-yaml": "^4.1.0", + "jscodeshift": "^17.3.0", "lodash": "^4.17.21", "nanoid": "3.3.8" }, @@ -31,6 +32,7 @@ "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^9.0.2", + "@web/rollup-plugin-copy": "^0.5.1", "babel-jest": "^29.7.0", "rimraf": "^5.0.7", "rollup": "3.2.5", diff --git a/packages/bruno-converters/rollup.config.js b/packages/bruno-converters/rollup.config.js index d0c0aad44..ec9a7a4c9 100644 --- a/packages/bruno-converters/rollup.config.js +++ b/packages/bruno-converters/rollup.config.js @@ -2,6 +2,7 @@ const { nodeResolve } = require('@rollup/plugin-node-resolve'); const commonjs = require('@rollup/plugin-commonjs'); const { terser } = require('rollup-plugin-terser'); const peerDepsExternal = require('rollup-plugin-peer-deps-external'); +const { copy } = require('@web/rollup-plugin-copy'); const packageJson = require('./package.json'); const alias = require('@rollup/plugin-alias'); @@ -12,12 +13,12 @@ module.exports = [ input: 'src/index.js', output: [ { - file: packageJson.main, + dir: path.dirname(packageJson.main), format: 'cjs', sourcemap: true }, { - file: packageJson.module, + dir: path.dirname(packageJson.module), format: 'esm', sourcemap: true } @@ -32,6 +33,10 @@ module.exports = [ terser(), alias({ entries: [{ find: 'src', replacement: path.resolve(__dirname, 'src') }] + }), + copy({ + patterns: 'src/workers/scripts/**/*', + rootDir: '.' }) ] } diff --git a/packages/bruno-converters/src/common/index.js b/packages/bruno-converters/src/common/index.js index c2a3d76aa..bc8c32cb4 100644 --- a/packages/bruno-converters/src/common/index.js +++ b/packages/bruno-converters/src/common/index.js @@ -47,6 +47,7 @@ export const validateSchema = (collection = {}) => { collectionSchema.validateSync(collection); return collection; } catch (err) { + console.log("Error validating schema", err); throw new Error('The Collection has an invalid schema'); } }; diff --git a/packages/bruno-converters/src/constants/index.js b/packages/bruno-converters/src/constants/index.js new file mode 100644 index 000000000..723a09fea --- /dev/null +++ b/packages/bruno-converters/src/constants/index.js @@ -0,0 +1,3 @@ +import { invalidVariableCharacterRegex } from './regex'; + +export { invalidVariableCharacterRegex }; diff --git a/packages/bruno-converters/src/constants/regex.js b/packages/bruno-converters/src/constants/regex.js new file mode 100644 index 000000000..989dbc1c3 --- /dev/null +++ b/packages/bruno-converters/src/constants/regex.js @@ -0,0 +1 @@ +export const invalidVariableCharacterRegex = /[^\w-.]/g; \ No newline at end of file diff --git a/packages/bruno-converters/src/index.js b/packages/bruno-converters/src/index.js index fa89457ed..d5b3d3a3b 100644 --- a/packages/bruno-converters/src/index.js +++ b/packages/bruno-converters/src/index.js @@ -2,4 +2,5 @@ export { default as postmanToBruno } from './postman/postman-to-bruno.js'; export { default as postmanToBrunoEnvironment } from './postman/postman-env-to-bruno-env.js'; export { default as brunoToPostman } from './postman/bruno-to-postman.js'; export { default as openApiToBruno } from './openapi/openapi-to-bruno.js'; -export { default as insomniaToBruno } from './insomnia/insomnia-to-bruno.js'; \ No newline at end of file +export { default as insomniaToBruno } from './insomnia/insomnia-to-bruno.js'; +export { default as postmanTranslation } from './postman/postman-translations.js'; \ No newline at end of file diff --git a/packages/bruno-converters/src/postman/postman-env-to-bruno-env.js b/packages/bruno-converters/src/postman/postman-env-to-bruno-env.js index c61c98d71..52d60b08b 100644 --- a/packages/bruno-converters/src/postman/postman-env-to-bruno-env.js +++ b/packages/bruno-converters/src/postman/postman-env-to-bruno-env.js @@ -1,4 +1,7 @@ import each from 'lodash/each'; +import { invalidVariableCharacterRegex } from '../constants'; +import { uuid } from '../common'; + const isSecret = (type) => { return type === 'secret'; }; @@ -8,7 +11,8 @@ const importPostmanEnvironmentVariables = (brunoEnvironment, values) => { each(values, (i) => { const brunoEnvironmentVariable = { - name: i.key, + uid: uuid(), + name: i.key.replace(invalidVariableCharacterRegex, '_'), value: i.value, enabled: i.enabled, secret: isSecret(i.type) diff --git a/packages/bruno-converters/src/postman/postman-to-bruno.js b/packages/bruno-converters/src/postman/postman-to-bruno.js index 31b2f3929..8ae8e195e 100644 --- a/packages/bruno-converters/src/postman/postman-to-bruno.js +++ b/packages/bruno-converters/src/postman/postman-to-bruno.js @@ -2,6 +2,7 @@ import get from 'lodash/get'; import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common'; import each from 'lodash/each'; import postmanTranslation from './postman-translations'; +import { invalidVariableCharacterRegex } from '../constants/index'; const parseGraphQLRequest = (graphqlSource) => { try { @@ -93,17 +94,10 @@ const importScriptsFromEvents = (events, requestObject) => { requestObject.script = {}; } - if (Array.isArray(event.script.exec)) { - if (event.script.exec.length > 0) { - requestObject.script.req = event.script.exec - .map((line) => postmanTranslation(line)) - .join('\n'); - } else { - requestObject.script.req = ''; - } - } else if (typeof event.script.exec === 'string') { - requestObject.script.req = postmanTranslation(event.script.exec); + if (event.script.exec && event.script.exec.length > 0) { + requestObject.script.req = postmanTranslation(event.script.exec) } else { + requestObject.script.req = ''; console.warn('Unexpected event.script.exec type', typeof event.script.exec); } } @@ -113,17 +107,10 @@ const importScriptsFromEvents = (events, requestObject) => { requestObject.tests = {}; } - if (Array.isArray(event.script.exec)) { - if (event.script.exec.length > 0) { - requestObject.tests = event.script.exec - .map((line) => postmanTranslation(line)) - .join('\n'); - } else { - requestObject.tests = ''; - } - } else if (typeof event.script.exec === 'string') { - requestObject.tests = postmanTranslation(event.script.exec); + if (event.script.exec && event.script.exec.length > 0) { + requestObject.tests = postmanTranslation(event.script.exec) } else { + requestObject.tests = ''; console.warn('Unexpected event.script.exec type', typeof event.script.exec); } } @@ -134,7 +121,7 @@ const importScriptsFromEvents = (events, requestObject) => { const importCollectionLevelVariables = (variables, requestObject) => { const vars = variables.map((v) => ({ uid: uuid(), - name: v.key, + name: v.key.replace(invalidVariableCharacterRegex, '_'), value: v.value, enabled: true })); @@ -246,13 +233,13 @@ const processAuth = (auth, requestObject) => { } }; -const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => { +const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, { useWorkers = false } = {}, scriptMap)=> { brunoParent.items = brunoParent.items || []; const folderMap = {}; const requestMap = {}; const requestMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE'] - each(item, (i) => { + item.forEach((i, index) => { if (isItemAFolder(i)) { const baseFolderName = i.name || 'Untitled Folder'; let folderName = baseFolderName; @@ -268,6 +255,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => { name: folderName, type: 'folder', items: [], + seq: index + 1, root: { docs: i.description || '', meta: { @@ -291,6 +279,8 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => { } }; + brunoParent.items.push(brunoFolderItem); + // Folder level auth if (i.auth) { processAuth(i.auth, brunoFolderItem.root.request); @@ -300,221 +290,221 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => { } if (i.item && i.item.length) { - importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth); + importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth, { useWorkers }, scriptMap); } if (i.event) { - importScriptsFromEvents(i.event, brunoFolderItem.root.request); + if(useWorkers) { + scriptMap.set(brunoFolderItem.uid, { + events: i.event, + request: brunoFolderItem.root.request + }); + } else { + importScriptsFromEvents(i.event, brunoFolderItem.root.request); + } } - brunoParent.items.push(brunoFolderItem); folderMap[folderName] = brunoFolderItem; - } else { - if (i.request) { - if (!requestMethods.includes(i?.request?.method.toUpperCase())) { - console.warn('Unexpected request.method', i?.request?.method); - return; + } else if (i.request) { + if (!requestMethods.includes(i?.request?.method.toUpperCase())) { + console.warn('Unexpected request.method', i?.request?.method); + return; + } + + const baseRequestName = i.name || 'Untitled Request'; + let requestName = baseRequestName; + let count = 1; + + while (requestMap[requestName]) { + requestName = `${baseRequestName}_${count}`; + count++; + } + + const url = constructUrl(i.request.url); + + const brunoRequestItem = { + uid: uuid(), + name: requestName, + type: 'http-request', + seq: index + 1, + request: { + url: url, + method: i?.request?.method?.toUpperCase(), + auth: { + mode: 'none', + basic: null, + bearer: null, + awsv4: null, + apikey: null, + oauth2: null, + digest: null + }, + headers: [], + params: [], + body: { + mode: 'none', + json: null, + text: null, + xml: null, + formUrlEncoded: [], + multipartForm: [] + }, + docs: i.request.description || '' } + }; - const baseRequestName = i.name || 'Untitled Request'; - let requestName = baseRequestName; - let count = 1; + brunoParent.items.push(brunoRequestItem); - while (requestMap[requestName]) { - requestName = `${baseRequestName}_${count}`; - count++; - } - - const url = constructUrl(i.request.url); - - const brunoRequestItem = { - uid: uuid(), - name: requestName, - type: 'http-request', - request: { - url: url, - method: i?.request?.method?.toUpperCase(), - auth: { - mode: 'none', - basic: null, - bearer: null, - awsv4: null, - apikey: null, - oauth2: null, - digest: null - }, - headers: [], - params: [], - body: { - mode: 'none', - json: null, - text: null, - xml: null, - formUrlEncoded: [], - multipartForm: [] - }, - docs: i.request.description || '' - } - }; - - if (i.event) { + if (i.event) { + if(useWorkers) { + scriptMap.set(brunoRequestItem.uid, { + events: i.event, + request: brunoRequestItem.request + }); + } else { i.event.forEach((event) => { if (event.listen === 'prerequest' && event.script && event.script.exec) { - if (!brunoRequestItem.request.script) { + if (!brunoRequestItem.request?.script) { brunoRequestItem.request.script = {}; } - if (Array.isArray(event.script.exec)) { - if (event.script.exec.length > 0) { - brunoRequestItem.request.script.req = event.script.exec - .map((line) => postmanTranslation(line)) - .join('\n'); - } else { - brunoRequestItem.request.script.req = ''; - } - } else if (typeof event.script.exec === 'string') { - brunoRequestItem.request.script.req = postmanTranslation(event.script.exec); + if (event.script.exec && event.script.exec.length > 0) { + brunoRequestItem.request.script.req = postmanTranslation(event.script.exec) } else { + brunoRequestItem.request.script.req = ''; console.warn('Unexpected event.script.exec type', typeof event.script.exec); } } if (event.listen === 'test' && event.script && event.script.exec) { - if (!brunoRequestItem.request.tests) { + if (!brunoRequestItem.request?.tests) { brunoRequestItem.request.tests = {}; } - if (Array.isArray(event.script.exec)) { - if (event.script.exec.length > 0) { - brunoRequestItem.request.tests = event.script.exec - .map((line) => postmanTranslation(line)) - .join('\n'); - } else { - brunoRequestItem.request.tests = ''; - } - } else if (typeof event.script.exec === 'string') { - brunoRequestItem.request.tests = postmanTranslation(event.script.exec); + if (event.script.exec && event.script.exec.length > 0) { + brunoRequestItem.request.tests = postmanTranslation(event.script.exec) } else { + brunoRequestItem.request.tests = ''; console.warn('Unexpected event.script.exec type', typeof event.script.exec); } } }); } - - const bodyMode = get(i, 'request.body.mode'); - if (bodyMode) { - if (bodyMode === 'formdata') { - brunoRequestItem.request.body.mode = 'multipartForm'; - - each(i.request.body.formdata, (param) => { - const isFile = param.type === 'file'; - let value; - let type; - - if (isFile) { - // If param.src is an array, keep it as it is. - // If param.src is a string, convert it into an array with a single element. - value = Array.isArray(param.src) ? param.src : typeof param.src === 'string' ? [param.src] : null; - type = 'file'; - } else { - value = param.value; - type = 'text'; - } - - brunoRequestItem.request.body.multipartForm.push({ - uid: uuid(), - type: type, - name: param.key, - value: value, - description: param.description, - enabled: !param.disabled - }); - }); - } - - if (bodyMode === 'urlencoded') { - brunoRequestItem.request.body.mode = 'formUrlEncoded'; - each(i.request.body.urlencoded, (param) => { - brunoRequestItem.request.body.formUrlEncoded.push({ - uid: uuid(), - name: param.key, - value: param.value, - description: param.description, - enabled: !param.disabled - }); - }); - } - - if (bodyMode === 'raw') { - let language = get(i, 'request.body.options.raw.language'); - if (!language) { - language = searchLanguageByHeader(i.request.header); - } - if (language === 'json') { - brunoRequestItem.request.body.mode = 'json'; - brunoRequestItem.request.body.json = i.request.body.raw; - } else if (language === 'xml') { - brunoRequestItem.request.body.mode = 'xml'; - brunoRequestItem.request.body.xml = i.request.body.raw; - } else { - brunoRequestItem.request.body.mode = 'text'; - brunoRequestItem.request.body.text = i.request.body.raw; - } - } - } - - if (bodyMode === 'graphql') { - brunoRequestItem.type = 'graphql-request'; - brunoRequestItem.request.body.mode = 'graphql'; - brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql); - } - - each(i.request.header, (header) => { - brunoRequestItem.request.headers.push({ - uid: uuid(), - name: header.key, - value: header.value, - description: header.description, - enabled: !header.disabled - }); - }); - - // Handle request-level auth or inherit from parent - const auth = i.request.auth ?? parentAuth; - processAuth(auth, brunoRequestItem.request); - - each(get(i, 'request.url.query'), (param) => { - brunoRequestItem.request.params.push({ - uid: uuid(), - name: param.key, - value: param.value, - description: param.description, - type: 'query', - enabled: !param.disabled - }); - }); - - each(get(i, 'request.url.variable', []), (param) => { - if (!param.key) { - // If no key, skip this iteration and discard the param - return; - } - - brunoRequestItem.request.params.push({ - uid: uuid(), - name: param.key, - value: param.value ?? '', - description: param.description ?? '', - type: 'path', - enabled: true - }); - }); - - brunoParent.items.push(brunoRequestItem); - requestMap[requestName] = brunoRequestItem; } + + const bodyMode = get(i, 'request.body.mode'); + if (bodyMode) { + if (bodyMode === 'formdata') { + brunoRequestItem.request.body.mode = 'multipartForm'; + + each(i.request.body.formdata, (param) => { + const isFile = param.type === 'file'; + let value; + let type; + + if (isFile) { + // If param.src is an array, keep it as it is. + // If param.src is a string, convert it into an array with a single element. + value = Array.isArray(param.src) ? param.src : typeof param.src === 'string' ? [param.src] : null; + type = 'file'; + } else { + value = param.value; + type = 'text'; + } + + brunoRequestItem.request.body.multipartForm.push({ + uid: uuid(), + type: type, + name: param.key, + value: value, + description: param.description, + enabled: !param.disabled + }); + }); + } + + if (bodyMode === 'urlencoded') { + brunoRequestItem.request.body.mode = 'formUrlEncoded'; + each(i.request.body.urlencoded, (param) => { + brunoRequestItem.request.body.formUrlEncoded.push({ + uid: uuid(), + name: param.key, + value: param.value, + description: param.description, + enabled: !param.disabled + }); + }); + } + + if (bodyMode === 'raw') { + let language = get(i, 'request.body.options.raw.language'); + if (!language) { + language = searchLanguageByHeader(i.request.header); + } + if (language === 'json') { + brunoRequestItem.request.body.mode = 'json'; + brunoRequestItem.request.body.json = i.request.body.raw; + } else if (language === 'xml') { + brunoRequestItem.request.body.mode = 'xml'; + brunoRequestItem.request.body.xml = i.request.body.raw; + } else { + brunoRequestItem.request.body.mode = 'text'; + brunoRequestItem.request.body.text = i.request.body.raw; + } + } + } + + if (bodyMode === 'graphql') { + brunoRequestItem.type = 'graphql-request'; + brunoRequestItem.request.body.mode = 'graphql'; + brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql); + } + + each(i.request.header, (header) => { + brunoRequestItem.request.headers.push({ + uid: uuid(), + name: header.key, + value: header.value, + description: header.description, + enabled: !header.disabled + }); + }); + + // Handle request-level auth or inherit from parent + const auth = i.request.auth ?? parentAuth; + processAuth(auth, brunoRequestItem.request); + + each(get(i, 'request.url.query'), (param) => { + brunoRequestItem.request.params.push({ + uid: uuid(), + name: param.key, + value: param.value, + description: param.description, + type: 'query', + enabled: !param.disabled + }); + }); + + each(get(i, 'request.url.variable', []), (param) => { + if (!param.key) { + // If no key, skip this iteration and discard the param + return; + } + + brunoRequestItem.request.params.push({ + uid: uuid(), + name: param.key, + value: param.value ?? '', + description: param.description ?? '', + type: 'path', + enabled: true + }); + }); + + requestMap[requestName] = brunoRequestItem; } }); }; + const searchLanguageByHeader = (headers) => { let contentType; each(headers, (header) => { @@ -530,7 +520,7 @@ const searchLanguageByHeader = (headers) => { return contentType; }; -const importPostmanV2Collection = (collection) => { +const importPostmanV2Collection = async (collection, { useWorkers = false }) => { const brunoCollection = { name: collection.info.name || 'Untitled Collection', uid: uuid(), @@ -571,12 +561,74 @@ const importPostmanV2Collection = (collection) => { // Collection level auth processAuth(collection.auth, brunoCollection.root.request); - importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth); - + // Create a single scriptMap for all items + const scriptMap = useWorkers ? new Map() : null; + + importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth, { useWorkers }, scriptMap); + + // Process all scripts in a single call at the top level + if (useWorkers && scriptMap && scriptMap.size > 0) { + try { + const { default: scriptTranslationWorker } = await import('../workers/postman-translator-worker'); + const translatedScripts = await scriptTranslationWorker(scriptMap); + + // Apply translated scripts to all items in the collection + const applyScriptsToItems = (items) => { + items.forEach(item => { + if (item.type === 'folder') { + // Apply scripts to the folder + if (translatedScripts.has(item.uid)) { + if (!item.root.request.script) { + item.root.request.script = {}; + } + if (!item.root.request.tests) { + item.root.request.tests = ''; + } + + const script = translatedScripts.get(item.uid).request?.script?.req; + const tests = translatedScripts.get(item.uid).request?.tests; + + item.root.request.script.req = script && script.length > 0 ? script : ''; + item.root.request.tests = tests && tests.length > 0 ? tests : ''; + } + + // Recursively apply to nested items + if (item.items && item.items.length > 0) { + applyScriptsToItems(item.items); + } + } else { + if (translatedScripts.has(item.uid)) { + if (!item.request.script) { + item.request.script = {}; + } + if (!item.request.tests) { + item.request.tests = ''; + } + + const script = translatedScripts.get(item.uid).request?.script?.req; + const tests = translatedScripts.get(item.uid).request?.tests; + + item.request.script.req = script && script.length > 0 ? script : ''; + item.request.tests = tests && tests.length > 0 ? tests : ''; + } + } + }); + }; + + applyScriptsToItems(brunoCollection.items); + + } catch (error) { + console.error('Error in script translation worker:', error); + } finally { + scriptMap.clear(); + } + } + return brunoCollection; }; -const parsePostmanCollection = (collection) => { + +const parsePostmanCollection = async (collection, { useWorkers = false }) => { try { let schema = get(collection, 'info.schema'); @@ -588,7 +640,7 @@ const parsePostmanCollection = (collection) => { ]; if (v2Schemas.includes(schema)) { - return importPostmanV2Collection(collection); + return await importPostmanV2Collection(collection, { useWorkers }); } throw new Error('Unsupported Postman schema version. Only Postman Collection v2.0 and v2.1 are supported.'); @@ -602,9 +654,10 @@ const parsePostmanCollection = (collection) => { } }; -const postmanToBruno = (postmanCollection) => { +const postmanToBruno = async (postmanCollection, { useWorkers = false } = {}) => { try { - const parsedPostmanCollection = parsePostmanCollection(postmanCollection); + + const parsedPostmanCollection = await parsePostmanCollection(postmanCollection, { useWorkers }); const transformedCollection = transformItemsInCollection(parsedPostmanCollection); const hydratedCollection = hydrateSeqInCollection(transformedCollection); const validatedCollection = validateSchema(hydratedCollection); @@ -615,4 +668,5 @@ const postmanToBruno = (postmanCollection) => { } }; + export default postmanToBruno; \ No newline at end of file diff --git a/packages/bruno-converters/src/postman/postman-translations.js b/packages/bruno-converters/src/postman/postman-translations.js index b741cd3b2..23195b5b8 100644 --- a/packages/bruno-converters/src/postman/postman-translations.js +++ b/packages/bruno-converters/src/postman/postman-translations.js @@ -1,3 +1,5 @@ +import translateCode from '../utils/jscode-shift-translator'; + const replacements = { 'pm\\.environment\\.get\\(': 'bru.getEnvVar(', 'pm\\.environment\\.set\\(': 'bru.setEnvVar(', @@ -20,7 +22,7 @@ const replacements = { 'pm\\.response\\.responseTime': 'res.getResponseTime()', 'pm\\.environment\\.name': 'bru.getEnvName()', 'pm\\.response\\.status': 'res.statusText', - 'pm\\.response\\.headers': 'req.getHeaders()', + 'pm\\.response\\.headers': 'res.getHeaders()', "tests\\['([^']+)'\\]\\s*=\\s*([^;]+);": 'test("$1", function() { expect(Boolean($2)).to.be.true; });', 'pm\\.request\\.url': 'req.getUrl()', 'pm\\.request\\.method': 'req.getMethod()', @@ -48,22 +50,38 @@ const compiledReplacements = Object.entries(extendedReplacements).map(([pattern, replacement })); -const postmanTranslation = (script) => { +const processRegexReplacement = (code) => { + for (const { regex, replacement } of compiledReplacements) { + if (regex.test(code)) { + code = code.replace(regex, replacement); + + } + } + if ((code.includes('pm.') || code.includes('postman.'))) { + code = code.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1'); + } + return code; +} + + +const postmanTranslation = (script, options = {}) => { + let modifiedScript = Array.isArray(script) ? script.join('\n') : script; + try { - let modifiedScript = script; - let modified = false; - for (const { regex, replacement } of compiledReplacements) { - if (regex.test(modifiedScript)) { - modifiedScript = modifiedScript.replace(regex, replacement); - modified = true; - } + let translatedCode = translateCode(modifiedScript); + if ((translatedCode.includes('pm.') || translatedCode.includes('postman.'))) { + translatedCode = translatedCode.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1'); } - if (modifiedScript.includes('pm.') || modifiedScript.includes('postman.')) { - modifiedScript = modifiedScript.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1'); - } - return modifiedScript; + return translatedCode; } catch (e) { - return script; + console.warn('Error in postman translation:', e); + + try { + return processRegexReplacement(modifiedScript); + } catch (e) { + console.warn('Error in postman translation:', e); + return modifiedScript; + } } }; diff --git a/packages/bruno-converters/src/utils/jscode-shift-translator.js b/packages/bruno-converters/src/utils/jscode-shift-translator.js new file mode 100644 index 000000000..924f4eb1d --- /dev/null +++ b/packages/bruno-converters/src/utils/jscode-shift-translator.js @@ -0,0 +1,680 @@ +const j = require('jscodeshift'); +const cloneDeep = require('lodash/cloneDeep'); + +/** + * Efficiently builds a string representation of a member expression without using toSource() + * + * @param {Object} node - The member expression node from the AST + * @return {string} - String representation of the member expression (e.g., "pm.environment.get") + */ +function getMemberExpressionString(node) { + // Handle base case: if this is an Identifier + if (node.type === 'Identifier') { + return node.name; + } + + // Handle member expressions + if (node.type === 'MemberExpression') { + const objectStr = getMemberExpressionString(node.object); + + // For computed properties like obj[prop], we need special handling + if (node.computed) { + // For literals like obj["prop"], we can include them in the string + if (node.property.type === 'Literal' && typeof node.property.value === 'string') { + return `${objectStr}.${node.property.value}`; + } + // For other computed properties, we can't reliably represent them as a simple string + return `${objectStr}.[computed]`; + } + + // For regular property access like obj.prop + if (node.property.type === 'Identifier') { + return `${objectStr}.${node.property.name}`; + } + } + + return '[unsupported]'; +} + +// Simple 1:1 translations for straightforward replacements +const simpleTranslations = { + // Environment variables + 'pm.environment.get': 'bru.getEnvVar', + 'pm.environment.set': 'bru.setEnvVar', + 'pm.environment.name': 'bru.getEnvName()', + 'pm.environment.unset': 'bru.deleteEnvVar', + + // Variables + 'pm.variables.get': 'bru.getVar', + 'pm.variables.set': 'bru.setVar', + 'pm.variables.has': 'bru.hasVar', + + // Collection variables + 'pm.collectionVariables.get': 'bru.getVar', + 'pm.collectionVariables.set': 'bru.setVar', + 'pm.collectionVariables.has': 'bru.hasVar', + 'pm.collectionVariables.unset': 'bru.deleteVar', + + // Request flow control + 'pm.setNextRequest': 'bru.setNextRequest', + + // Testing + 'pm.test': 'test', + 'pm.expect': 'expect', + 'pm.expect.fail': 'expect.fail', + + // Request properties + 'pm.request.url': 'req.getUrl()', + 'pm.request.method': 'req.getMethod()', + 'pm.request.headers': 'req.getHeaders()', + 'pm.request.body': 'req.getBody()', + + // Response properties + 'pm.response.json': 'res.getBody', + 'pm.response.code': 'res.getStatus()', + 'pm.response.status': 'res.statusText', + 'pm.response.responseTime': 'res.getResponseTime()', + 'pm.response.statusText': 'res.statusText', + 'pm.response.headers': 'res.getHeaders()', + + // Execution control + 'pm.execution.skipRequest': 'bru.runner.skipRequest', + + // Legacy Postman API (deprecated) (we can use pm instead of postman, as we are converting all postman references to pm in the code as the part of pre-processing) + 'pm.setEnvironmentVariable': 'bru.setEnvVar', + 'pm.getEnvironmentVariable': 'bru.getEnvVar', + 'pm.clearEnvironmentVariable': 'bru.deleteEnvVar', +}; + +/* Complex transformations that need custom handling +* Note: Transform functions can return either a single node or an array of nodes. +* When returning an array of nodes, each node in the array will be inserted +* as a separate statement, which allows a single Postman expression to be +* transformed into multiple Bruno statements (e.g. for complex assertions). +*/ +const complexTransformations = [ + // pm.environment.has requires special handling + { + pattern: 'pm.environment.has', + transform: (path, j) => { + const callExpr = path.parent.value; + + const args = callExpr.arguments; + + // Create: bru.getEnvVar(arg) !== undefined && bru.getEnvVar(arg) !== null + return j.logicalExpression( + '&&', + j.binaryExpression( + '!==', + j.callExpression(j.identifier('bru.getEnvVar'), args), + j.identifier('undefined') + ), + j.binaryExpression( + '!==', + j.callExpression(j.identifier('bru.getEnvVar'), args), + j.identifier('null') + ) + ); + } + }, + + { + pattern: 'pm.response.text', + transform: (_, j) => { + return j.callExpression(j.identifier('JSON.stringify'), [j.identifier('res.getBody()')]); + } + }, + + // Handle pm.response.to.have.status + { + pattern: 'pm.response.to.have.status', + transform: (path, j) => { + const callExpr = path.parent.value; + + const args = callExpr.arguments; + + // Create: expect(res.getStatus()).to.equal(arg) + return j.callExpression( + j.memberExpression( + j.callExpression( + j.identifier('expect'), + [ + j.callExpression( + j.identifier('res.getStatus'), + [] + ) + ] + ), + j.identifier('to.equal') + ), + args + ); + } + }, + + // handle 'pm.response.to.have.header' to expect(res.getHeaders()).to.have.property(args) + { + pattern: 'pm.response.to.have.header', + transform: (path, j) => { + const callExpr = path.parent.value; + + const args = callExpr.arguments; + + + if (args.length > 0) { + // Apply toLowerCase() to the first argument + args[0] = j.callExpression( + j.memberExpression( + args[0], + j.identifier('toLowerCase') + ), + [] + ); + } + + // Create: expect(res.getHeaders()).to.have.property(args) + return j.callExpression( + j.memberExpression( + j.callExpression( + j.identifier('expect'), + [ + j.callExpression( + j.identifier('res.getHeaders'), + [] + ) + ] + ), + j.identifier('to.have.property') + ), + args + ); + + } + }, + + // Handle pm.execution.setNextRequest(null) + { + pattern: 'pm.execution.setNextRequest', + transform: (path, j) => { + const callExpr = path.parent.value; + + const args = callExpr.arguments; + + // If argument is null or 'null', transform to bru.runner.stopExecution() + if ( + args[0].type === 'Literal' && (args[0].value === null || args[0].value === 'null') + ) { + return j.callExpression( + j.identifier('bru.runner.stopExecution'), + [] + ); + } + + // Otherwise, keep as bru.runner.setNextRequest with the same argument + return j.callExpression( + j.identifier('bru.runner.setNextRequest'), + args + ); + } + }, +]; + +// Create a map for complex transformations to enable O(1) lookups +const complexTransformationsMap = {}; +complexTransformations.forEach(transform => { + complexTransformationsMap[transform.pattern] = transform; +}); + +const varInitsToReplace = new Set(['pm', 'postman', 'pm.request','pm.response', 'pm.test', 'pm.expect', 'pm.environment', 'pm.variables', 'pm.collectionVariables', 'pm.execution']); + +/** + * Process all transformations (both simple and complex) in the AST in a single pass + * @param {Object} ast - jscodeshift AST + * @param {Set} transformedNodes - Set of already transformed nodes + */ +function processTransformations(ast, transformedNodes) { + ast.find(j.MemberExpression).forEach(path => { + if (transformedNodes.has(path.node)) return; + + // Get string representation using our utility function + const memberExprStr = getMemberExpressionString(path.value); + + // First check for simple transformations (O(1)) + if (simpleTranslations.hasOwnProperty(memberExprStr)) { + const replacement = simpleTranslations[memberExprStr]; + j(path).replaceWith(j.identifier(replacement)); + transformedNodes.add(path.node); + return; // Skip complex transformation check if simple transformation applied + } + + // Then check for complex transformations (O(1)) + if (complexTransformationsMap.hasOwnProperty(memberExprStr) && + path.parent.value.type === 'CallExpression') { + const transform = complexTransformationsMap[memberExprStr]; + const replacement = transform.transform(path, j); + if (Array.isArray(replacement)) { + replacement.forEach((nodePath, index) => { + if(index === 0) { + j(path.parent).replaceWith(nodePath); + } else { + j(path.parent.parent).insertAfter(nodePath); + } + transformedNodes.add(nodePath.node); + transformedNodes.add(path.parent.node); + }); + } else { + j(path.parent).replaceWith(replacement); + transformedNodes.add(path.node); + transformedNodes.add(path.parent.node); + } + } + }); +} + +/** + * Translates Postman script code to Bruno script code + * @param {string} code - The Postman script code to translate + * @returns {string} The translated Bruno script code + */ +function translateCode(code) { + // Replace 'postman' with 'pm' using regex before creating the AST + // This is more efficient than an AST traversal + code = code.replace(/\bpostman\b/g, 'pm'); + + const ast = j(code); + + // Keep track of transformed nodes to avoid double-processing + const transformedNodes = new Set(); + + // Preprocess the code to resolve all aliases + preprocessAliases(ast); + + // Process all transformations in a single pass + processTransformations(ast, transformedNodes); + + // Handle special Postman syntax patterns + handleTestsBracketNotation(ast); + + return ast.toSource(); +} + +/** + * Preprocess all variable aliases in the AST to simplify later transformations + * @param {Object} ast - jscodeshift AST + */ +function preprocessAliases(ast) { + // Create a symbol table to track what each variable references + const symbolTable = new Map(); + const MAX_ITERATIONS = 5; + let iterations = 0; + + // Keep preprocessing until no more changes can be made + let changesMade; + do { + changesMade = false; + + // First pass: Identify all variables + findVariableDefinitions(ast, symbolTable); + + // Second pass: Replace all variable references with their resolved values + changesMade = resolveVariableReferences(ast, symbolTable) || false; + + // Third pass: Clean up variable declarations that are no longer needed + changesMade = removeResolvedDeclarations(ast, symbolTable) || false; + + iterations++; + + } while (changesMade && iterations < MAX_ITERATIONS); +} + +/** + * Find all variable definitions and track what they reference + * @param {Object} ast - jscodeshift AST + * @param {Map} symbolTable - Map to track variable references + */ +function findVariableDefinitions(ast, symbolTable) { + // Use a single traversal to handle both direct assignments and object destructuring + ast.find(j.VariableDeclarator).forEach(path => { + // Only process nodes that have an initializer + if (!path.value.init) return; + + // Handle direct assignments: const response = pm.response + if (path.value.id.type === 'Identifier') { + const varName = path.value.id.name; + + // If it's a direct identifier, just map it + if (path.value.init.type === 'Identifier') { + symbolTable.set(varName, { + type: 'identifier', + value: path.value.init.name + }); + } + // If it's a member expression, store both parts + else if (path.value.init.type === 'MemberExpression') { + const sourceCode = getMemberExpressionString(path.value.init); + symbolTable.set(varName, { + type: 'memberExpression', + value: sourceCode, + node: path.value.init + }); + } + } + // Handle object destructuring: const { response } = pm + else if (path.value.id.type === 'ObjectPattern' && path.value.init.type === 'Identifier') { + const source = path.value.init.name; + + path.value.id.properties.forEach(prop => { + if (prop.key.name && prop.value.type === 'Identifier') { + const destVarName = prop.value.name; + symbolTable.set(destVarName, { + type: 'memberExpression', + value: `${source}.${prop.key.name}`, + node: j.memberExpression( + j.identifier(source), + j.identifier(prop.key.name) + ) + }); + } + }); + } + }); +} + +/** + * Resolve variable references by replacing them with their original values + * @param {Object} ast - jscodeshift AST + * @param {Map} symbolTable - Map of variable references + * @returns {boolean} Whether any changes were made + */ +function resolveVariableReferences(ast, symbolTable) { + let changesMade = false; + + /** + * Example of what this function does: + * + * Input Postman code: + * const response = pm.response; + * const jsonData = response.json(); // response is a reference to pm.response + * + * After resolution: + * const response = pm.response; + * const jsonData = pm.response.json(); // response reference is replaced with pm.response + * + * Then in the next preprocessing phase, unnecessary variables like 'response' will be removed. + */ + + // Replace all identifier references with their resolved values + ast.find(j.Identifier).forEach(path => { + const varName = path.value.name; + + /** + * Skip specific types of identifiers that shouldn't be replaced: + * + * Case 1: Variable definitions (left side of declarations) + * ----------------------------------------------------- + * In code like: + * const response = pm.response; + * ^ + * We shouldn't replace 'response' on the left side with pm.response, + * which would result in: const pm.response = pm.response; (invalid syntax) + * + * Case 2: Property names in member expressions + * ----------------------------------------------------- + * In code like: + * console.log(response.status); + * ^ + * We shouldn't replace the 'status' property name with anything, + * only the 'response' object reference should be replaced. + * + * We only want to replace identifiers that are being used as references, + * not the ones being defined or used as property names. + */ + + // Skip if this is a variable definition or property name + if (path.parent.value.type === 'VariableDeclarator' && path.parent.value.id === path.value) { + return; + } + if (path.parent.value.type === 'MemberExpression' && path.parent.value.property === path.value && !path.parent.value.computed) { + return; + } + + // Only replace if this is a known variable + if (!symbolTable.has(varName)) return; + + const symbolInfo = symbolTable.get(varName); + if(!varInitsToReplace.has(symbolInfo.value)) { + return; + } + const newNode = cloneDeep(symbolInfo.node); + j(path).replaceWith(newNode); + symbolTable.set(varName, { + type: 'memberExpression', + value: symbolInfo.value, + node: newNode + }); + changesMade = true; + + }); + + return changesMade; +} + +/** + * Remove variable declarations that have been resolved + * @param {Object} ast - jscodeshift AST + * @param {Map} symbolTable - Map of variable references + * @returns {boolean} Whether any changes were made + */ +function removeResolvedDeclarations(ast, symbolTable) { + let changesMade = false; + + /** + * Example of what this function does: + * + * Original Postman code: + * const response = pm.response; + * const jsonData = response.json(); + * console.log(jsonData.name); + * + * After variable resolution: + * const response = pm.response; // This declaration is now redundant + * const jsonData = pm.response.json(); // This value has been resolved + * console.log(jsonData.name); // This still references jsonData + * + * Final code after this cleanup step: + * const jsonData = pm.response.json(); // response variable declaration is removed + * console.log(jsonData.name); // jsonData is kept since it's still referenced + * + * We only remove declarations that: + * 1. Have been fully resolved (references to pm.* objects) + * 2. No longer provide any value (since all references were replaced with resolved values) + */ + + // Use a single traversal to handle both regular variable declarations and destructuring + ast.find(j.VariableDeclarator).forEach(path => { + // Case 1: Handle regular variable declarations + if (path.value.id.type === 'Identifier') { + const varName = path.value.id.name; + const replacement = symbolTable.get(varName); + if(!replacement || !varInitsToReplace.has(replacement.value)) return; + + /** + * This code differentiates between two types of variable declarations: + * + * Example 1: Single variable declaration + * ----------------------------------- + * Input: const response = pm.response; + * Action: The entire statement can be removed + * Output: [statement removed] + * + * Example 2: Multiple variables in one declaration + * ----------------------------------- + * Input: const response = pm.response, unrelated = 5; + * Action: Only remove the 'response' declarator, keep the others + * Output: const unrelated = 5; + * + * We need this distinction to ensure we don't accidentally remove + * unrelated variables that happen to be declared in the same statement. + */ + const declarationPath = j(path).closest(j.VariableDeclaration); + if (declarationPath.get().value.declarations.length === 1) { + declarationPath.remove(); + } else { + // Otherwise just remove this declarator + j(path).remove(); + } + + changesMade = true; + } + // Case 2: Handle destructuring of pm + else if (path.value.id.type === 'ObjectPattern' && + path.value.init && + path.value.init.type === 'Identifier' && + path.value.init.name === 'pm') { + + /** + * Example of destructuring removal: + * + * Original Postman code: + * const { response, environment } = pm; + * console.log(response.json().name); + * console.log(environment.get("variable")); + * + * After variable resolution steps: + * const { response, environment } = pm; // This destructuring is now redundant + * console.log(pm.response.json().name); // 'response' references already replaced with pm.response + * console.log(pm.environment.get("variable")); // 'environment' references replaced + * + * Final code after this cleanup step: + * console.log(pm.response.json().name); // Destructuring declaration is completely removed + * console.log(pm.environment.get("variable")); + * + * This step specifically targets the Postman pattern of destructuring the pm object, + * which is common in Postman scripts but needs to be removed in the Bruno conversion. + */ + + const declarationPath = j(path).closest(j.VariableDeclaration); + if (declarationPath.get().value.declarations.length === 1) { + declarationPath.remove(); + } else { + j(path).remove(); + } + + changesMade = true; + } + }); + + return changesMade; +} + +/** + * Handle Postman's tests["..."] = ... syntax + * @param {Object} ast - jscodeshift AST + */ +function handleTestsBracketNotation(ast) { + // Find the ExpressionStatement that contains the assignment + ast.find(j.ExpressionStatement, { + expression: { + type: 'AssignmentExpression', + left: { + type: 'MemberExpression', + object: { name: 'tests' }, + computed: true, + property: {} // Accept any property type + } + } + }).forEach(path => { + // Get the assignment expression + const assignment = path.value.expression; + const left = assignment.left; + + // Verify it's a valid tests[] expression + if (left.object.type === 'Identifier' && + left.object.name === 'tests' && + left.computed === true) { + + const property = left.property; + const rightSide = assignment.right; + + // Handle string literals + if (property.type === 'Literal' && typeof property.value === 'string') { + const testName = property.value; + + // Replace with test() function call + j(path).replaceWith( + j.expressionStatement( + j.callExpression( + j.identifier('test'), + [ + j.literal(testName), + j.functionExpression( + null, + [], + j.blockStatement([ + j.expressionStatement( + j.memberExpression( + j.callExpression( + j.identifier('expect'), + [ + j.callExpression( + j.identifier('Boolean'), + [rightSide] + ) + ] + ), + j.identifier('to.be.true') + ) + ) + ]) + ) + ] + ) + ) + ); + } + // Handle template literals + else if (property.type === 'TemplateLiteral') { + // Create a template literal with the same quasi and expressions + const templateLiteral = j.templateLiteral( + property.quasis, + property.expressions + ); + + // Replace with test() function call using template literal + j(path).replaceWith( + j.expressionStatement( + j.callExpression( + j.identifier('test'), + [ + templateLiteral, + j.functionExpression( + null, + [], + j.blockStatement([ + j.expressionStatement( + j.memberExpression( + j.callExpression( + j.identifier('expect'), + [ + j.callExpression( + j.identifier('Boolean'), + [rightSide] + ) + ] + ), + j.identifier('to.be.true') + ) + ) + ]) + ) + ] + ) + ) + ); + } + } + }); +} + +export { getMemberExpressionString }; +export default translateCode; \ No newline at end of file diff --git a/packages/bruno-converters/src/workers/postman-translator-worker.js b/packages/bruno-converters/src/workers/postman-translator-worker.js new file mode 100644 index 000000000..1c9ebea79 --- /dev/null +++ b/packages/bruno-converters/src/workers/postman-translator-worker.js @@ -0,0 +1,211 @@ +const { Worker } = require('node:worker_threads'); +const path = require('node:path'); +const os = require('node:os'); + +function getMaxWorkers() { + return Math.max(os.availableParallelism(), 1) +} + +class WorkerPool { + constructor(scriptPath, size) { + this.workers = []; + this.idle = []; + this.queue = []; + this.scriptPath = scriptPath; + this.size = size; + } + + // Initialize the worker pool + initialize() { + for (let i = 0; i < this.size; i++) { + const worker = new Worker(this.scriptPath); + this.workers.push(worker); + this.idle.push(i); + } + } + + // Run a task on a worker + runTask(data) { + return new Promise((resolve, reject) => { + const task = { data, resolve, reject }; + + if (this.idle.length > 0) { + this._runTaskOnWorker(this.idle.shift(), task); + } else { + this.queue.push(task); + } + }); + } + + // Run a task on a specific worker + _runTaskOnWorker(workerId, task) { + const worker = this.workers[workerId]; + + const messageHandler = (result) => { + // Cleanup listeners + worker.removeListener('message', messageHandler); + worker.removeListener('error', errorHandler); + + // Mark worker as idle + this.idle.push(workerId); + + // Process queue if tasks are waiting + if (this.queue.length > 0) { + this._runTaskOnWorker(workerId, this.queue.shift()); + } + + // Resolve the task + task.resolve(result); + }; + + const errorHandler = (err) => { + worker.removeListener('message', messageHandler); + worker.removeListener('error', errorHandler); + + this.idle.push(workerId); + + if (this.queue.length > 0) { + this._runTaskOnWorker(workerId, this.queue.shift()); + } + + task.reject(err); + }; + + worker.on('message', messageHandler); + worker.on('error', errorHandler); + worker.postMessage(task.data); + } + + // Terminate all workers + terminate() { + for (const worker of this.workers) { + worker.terminate(); + } + this.workers = []; + this.idle = []; + } +} + +// Helper function to count lines in a script +function countScriptLines(script) { + if (!script) return 0; + return Array.isArray(script) ? script.length : script.split('\n').length; +} + +// Calculate complexity of a script entry +function calculateScriptComplexity([uid, entry]) { + let totalLines = 0; + const { events } = entry + + if (events && Array.isArray(events)) { + events.forEach(({ script }) => { + if (script && script.exec) { + totalLines += countScriptLines(script.exec); + } + }); + } + + return { uid, entry, complexity: totalLines || 1 }; // Minimum complexity of 1 +} + +// Create balanced batches based on script complexity +function createBalancedBatches(scriptEntries, workerCount) { + // Calculate complexity for each script + const scriptsWithComplexity = scriptEntries.map(calculateScriptComplexity); + + // Sort scripts by complexity (descending) + scriptsWithComplexity.sort((a, b) => b.complexity - a.complexity); + + // Initialize batches + const batches = Array.from({ length: workerCount }, () => ({ + entries: [], + totalComplexity: 0 + })); + + // Algorithm: Greedy load balancing + // 1. Process scripts in descending order of complexity + // 2. Always assign each script to the batch with lowest current load + // 3. This minimizes the maximum workload across all workers + for (const { uid, entry, complexity } of scriptsWithComplexity) { + + const batchWithLowestComplexity = batches.reduce( + (target, current) => current.totalComplexity < target.totalComplexity ? current : target + ); + + // Add the script to this batch + batchWithLowestComplexity.entries.push({uid, entry}); + batchWithLowestComplexity.totalComplexity += complexity; + } + + return batches.map(batch => + batch.entries.map(({ uid, entry }) => [uid, entry]) + ).filter(batch => batch.length > 0); +} + +const scriptTranslationWorker = async (scriptMap) => { + // Convert the Map to an array of entries + const scriptEntries = Array.from(scriptMap.entries()); + const maxWorkers = getMaxWorkers(); + + // For very small collections, don't parallelize + if (scriptEntries.length <= 50) { + const workerPool = new WorkerPool(path.join(__dirname,'./src/workers/scripts/translate-postman-scripts.js'), 1); + workerPool.initialize(); + + try { + const translatedScripts = new Map(); + const result = await workerPool.runTask({ scripts: scriptEntries }); + + if (result.error) { + console.error('Error in script translation worker:', result.error); + throw new Error(result.error); + } + + result.forEach(([uid, { request }]) => { + translatedScripts.set(uid, { request }); + }); + + return translatedScripts; + } finally { + workerPool.terminate(); + } + } + + + const workerCount = Math.min(maxWorkers, 4); + + // Create balanced batches based on script complexity + const batches = createBalancedBatches(scriptEntries, workerCount); + + const translatedScripts = new Map(); + + // Create worker pool with optimal size + const workerPool = new WorkerPool(path.join(__dirname,'./src/workers/scripts/translate-postman-scripts.js'), workerCount); + workerPool.initialize(); + + // Process all batches in parallel using worker pool + const batchPromises = batches.map(batch => { + return workerPool.runTask({ scripts: batch }) + .then(modScripts => { + modScripts.forEach(([name, { request }]) => { + translatedScripts.set(name, { request }); + }); + }) + .catch(err => { + console.error('Error in script translation worker:', err); + throw new Error(err); + }); + }); + + // Wait for all batches to complete + try { + await Promise.allSettled(batchPromises); + } finally { + // Clean up worker pool + workerPool.terminate(); + } + + return translatedScripts; +}; + +export default scriptTranslationWorker \ No newline at end of file diff --git a/packages/bruno-converters/src/workers/scripts/translate-postman-scripts.js b/packages/bruno-converters/src/workers/scripts/translate-postman-scripts.js new file mode 100644 index 000000000..31b7d9008 --- /dev/null +++ b/packages/bruno-converters/src/workers/scripts/translate-postman-scripts.js @@ -0,0 +1,44 @@ +const { parentPort } = require('node:worker_threads'); +const { postmanTranslation } = require('@usebruno/converters'); + +parentPort.on('message', (workerData) => { + try { + const { scripts } = workerData; + const modScripts = scripts.map(([uid, { events }]) => { + const requestObject = { + script: {}, + tests: {} + } + + if (events && Array.isArray(events)) { + events.forEach((event) => { + if(event?.script && event.script.exec) { + if(event.listen === 'prerequest') { + if(event.script.exec && event.script.exec.length > 0) { + requestObject.script.req = postmanTranslation(event.script.exec); + } else { + requestObject.script.req = ''; + } + } + + if(event.listen === 'test') { + if(event.script.exec && event.script.exec.length > 0) { + requestObject.tests = postmanTranslation(event.script.exec); + } else { + requestObject.tests = ''; + } + } + } + }); + } + + return [uid, { request: requestObject }]; + }); + + parentPort.postMessage(modScripts); + } + catch(error) { + console.error(error); + parentPort.postMessage({ error: error?.message }); + } +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-env-to-bruno-env.spec.js b/packages/bruno-converters/tests/postman/postman-env-to-bruno-env.spec.js index 665a52e75..6101548bd 100644 --- a/packages/bruno-converters/tests/postman/postman-env-to-bruno-env.spec.js +++ b/packages/bruno-converters/tests/postman/postman-env-to-bruno-env.spec.js @@ -32,12 +32,14 @@ describe('postmanToBrunoEnvironment Function', () => { value: 'value1', enabled: true, secret: false, + uid: "mockeduuidvalue123456", }, { name: 'var2', value: 'value2', enabled: false, secret: true, + uid: "mockeduuidvalue123456", }, ], }; diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js index ef98774de..d1a5caa7a 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js @@ -2,7 +2,7 @@ import { describe, it, expect } from '@jest/globals'; import postmanToBruno from '../../../src/postman/postman-to-bruno'; describe('Collection Authentication', () => { - it('should handle basic auth at collection level', () => { + it('should handle basic auth at collection level', async() => { const postmanCollection = { info: { name: 'Collection level basic auth', @@ -44,7 +44,7 @@ describe('Collection Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); // console.log('result', JSON.stringify(result, null, 2)); expect(result.root.request.auth).toEqual({ @@ -61,7 +61,7 @@ describe('Collection Authentication', () => { }); }); - it('should handle bearer token auth at collection level', () => { + it('should handle bearer token auth at collection level', async() => { const postmanCollection = { info: { name: 'Collection level bearer token', @@ -98,7 +98,7 @@ describe('Collection Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); // console.log('result', JSON.stringify(result, null, 2)); expect(result.root.request.auth).toEqual({ @@ -112,9 +112,9 @@ describe('Collection Authentication', () => { oauth2: null, digest: null }); - }); + }); - it('should handle API key auth at collection level', () => { + it('should handle API key auth at collection level', async() => { const postmanCollection = { info: { name: 'Collection level api key', @@ -156,7 +156,7 @@ describe('Collection Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.root.request.auth).toEqual({ mode: 'apikey', @@ -173,7 +173,7 @@ describe('Collection Authentication', () => { }); }); - it('should handle digest auth at collection level', () => { + it('should handle digest auth at collection level', async() => { const postmanCollection = { info: { name: 'Collection level digest auth', @@ -220,7 +220,7 @@ describe('Collection Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.root.request.auth).toEqual({ mode: 'digest', diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js index b403d22d8..ba6f86596 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js @@ -2,7 +2,7 @@ import { describe, it, expect } from '@jest/globals'; import postmanToBruno from '../../../src/postman/postman-to-bruno'; describe('Folder Authentication', () => { - it('should handle basic auth at folder level', () => { + it('should handle basic auth at folder level', async() => { const postmanCollection = { info: { name: 'Folder level basic auth', @@ -49,7 +49,7 @@ describe('Folder Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.items[0].root.request.auth).toEqual({ mode: 'basic', @@ -65,7 +65,7 @@ describe('Folder Authentication', () => { }); }); - it('should handle bearer token auth at folder level', () => { + it('should handle bearer token auth at folder level', async() => { const postmanCollection = { info: { name: 'Folder level bearer token', @@ -107,7 +107,7 @@ describe('Folder Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.items[0].root.request.auth).toEqual({ mode: 'bearer', @@ -120,7 +120,7 @@ describe('Folder Authentication', () => { }); }); - it('should handle API key auth at folder level', () => { + it('should handle API key auth at folder level', async() => { const postmanCollection = { info: { name: 'Folder level API key', @@ -167,7 +167,7 @@ describe('Folder Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.items[0].root.request.auth).toEqual({ mode: 'apikey', @@ -180,7 +180,7 @@ describe('Folder Authentication', () => { }); }); - it('should handle digest auth at folder level', () => { + it('should handle digest auth at folder level', async() => { const postmanCollection = { info: { name: 'Folder level digest auth', @@ -232,7 +232,7 @@ describe('Folder Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.items[0].root.request.auth).toEqual({ mode: 'digest', diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js index 7eac3906c..3ac79476c 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js @@ -3,7 +3,7 @@ import postmanToBruno from '../../../src/postman/postman-to-bruno'; describe('postman-collection', () => { it('should correctly import a valid Postman collection file', async () => { - const brunoCollection = postmanToBruno(postmanCollection); + const brunoCollection = await postmanToBruno(postmanCollection); expect(brunoCollection).toMatchObject(expectedOutput); }); }); @@ -73,92 +73,93 @@ const expectedOutput = { "version": "1", "items": [ { - "uid": "mockeduuidvalue123456", - "name": "folder", - "type": "folder", - "items": [ - { - "uid": "mockeduuidvalue123456", - "name": "request", - "type": "http-request", - "request": { - "url": "https://usebruno.com", - "method": "GET", - "auth": { - "mode": "none", - "basic": null, - "bearer": null, - "awsv4": null, - "apikey": null, - "oauth2": null, - "digest": null - }, - "headers": [], - "params": [], - "body": { - "mode": "none", - "json": null, - "text": null, - "xml": null, - "formUrlEncoded": [], - "multipartForm": [] - }, - "docs": "" - }, - "seq": 1 - } - ], - "root": { - "docs": "", - "meta": { - "name": "folder" - }, - "request": { - "auth": { - "mode": "none", - "basic": null, - "bearer": null, - "awsv4": null, - "apikey": null, - "oauth2": null, - "digest": null - }, - "headers": [], - "script": {}, - "tests": "", - "vars": {} - } - } + "uid": "mockeduuidvalue123456", + "name": "folder", + "type": "folder", + "seq": 1, + "items": [ + { + "uid": "mockeduuidvalue123456", + "name": "request", + "type": "http-request", + "seq": 1, + "request": { + "url": "https://usebruno.com", + "method": "GET", + "auth": { + "mode": "none", + "basic": null, + "bearer": null, + "awsv4": null, + "apikey": null, + "oauth2": null, + "digest": null + }, + "headers": [], + "params": [], + "body": { + "mode": "none", + "json": null, + "text": null, + "xml": null, + "formUrlEncoded": [], + "multipartForm": [] + }, + "docs": "" + } + } + ], + "root": { + "docs": "", + "meta": { + "name": "folder" + }, + "request": { + "auth": { + "mode": "none", + "basic": null, + "bearer": null, + "awsv4": null, + "apikey": null, + "oauth2": null, + "digest": null + }, + "headers": [], + "script": {}, + "tests": "", + "vars": {} + } + } }, { - "uid": "mockeduuidvalue123456", - "name": "request", - "type": "http-request", - "request": { - "url": "https://usebruno.com", - "method": "GET", - "auth": { - "mode": "none", - "basic": null, - "bearer": null, - "awsv4": null, - "apikey": null, - "oauth2": null, - "digest": null - }, - "headers": [], - "params": [], - "body": { - "mode": "none", - "json": null, - "text": null, - "xml": null, - "formUrlEncoded": [], - "multipartForm": [] - }, - "docs": "" - }, - "seq": 1 + "uid": "mockeduuidvalue123456", + "name": "request", + "type": "http-request", + "seq": 2, + "request": { + "url": "https://usebruno.com", + "method": "GET", + "auth": { + "mode": "none", + "basic": null, + "bearer": null, + "awsv4": null, + "apikey": null, + "oauth2": null, + "digest": null + }, + "headers": [], + "params": [], + "body": { + "mode": "none", + "json": null, + "text": null, + "xml": null, + "formUrlEncoded": [], + "multipartForm": [] + }, + "docs": "" + }, } ], "environments": [], diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-response.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-response.spec.js index 4f365589c..a57b8435a 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-response.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-response.spec.js @@ -20,7 +20,7 @@ describe('postmanTranslations - response commands', () => { const responseText = JSON.stringify(res.getBody()); const responseJson = res.getBody(); const responseStatus = res.statusText; - const responseHeaders = req.getHeaders(); + const responseHeaders = res.getHeaders(); test('Status code is 200', function() { expect(res.getStatus()).to.equal(200); diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js index a542a6b61..2a71301d3 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js @@ -2,7 +2,7 @@ import { describe, it, expect } from '@jest/globals'; import postmanToBruno from '../../../src/postman/postman-to-bruno'; describe('Request Authentication', () => { - it('should handle basic auth at request level', () => { + it('should handle basic auth at request level', async() => { const postmanCollection = { info: { name: 'Request Auth Collection', @@ -26,7 +26,7 @@ describe('Request Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.items[0].request.auth).toEqual({ mode: 'basic', @@ -42,7 +42,7 @@ describe('Request Authentication', () => { }); }); - it('should inherit folder auth when request has no auth', () => { + it('should inherit folder auth when request has no auth', async() => { const postmanCollection = { info: { name: 'Inherit Request Auth Collection', @@ -68,7 +68,7 @@ describe('Request Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.items[0].items[0].request.auth).toEqual({ mode: 'bearer', @@ -83,7 +83,7 @@ describe('Request Authentication', () => { }); }); - it('should override folder auth with request auth', () => { + it('should override folder auth with request auth', async() => { const postmanCollection = { info: { name: 'Override Request Auth Collection', @@ -116,7 +116,7 @@ describe('Request Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.items[0].items[0].request.auth).toEqual({ mode: 'bearer', diff --git a/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js b/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js index 6d98ce6e8..1c1686bf2 100644 --- a/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js +++ b/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js @@ -17,7 +17,7 @@ describe('postmanTranslations - comment handling', () => { test('should comment non-translated pm commands', () => { const inputScript = "pm.test('random test', () => postman.variables.replaceIn('{{$guid}}'));"; - const expectedOutput = "// test('random test', () => postman.variables.replaceIn('{{$guid}}'));"; + const expectedOutput = "// test('random test', () => pm.variables.replaceIn('{{$guid}}'));"; expect(postmanTranslation(inputScript)).toBe(expectedOutput); }); diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/combined.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/combined.test.js new file mode 100644 index 000000000..8d3508e05 --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/combined.test.js @@ -0,0 +1,418 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Combined API Features Translation', () => { + // Basic translation test + it('should translate code', () => { + const code = 'console.log("Hello, world!");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(code); + }); + + // Preserving comments + it('should preserve comments', () => { + const code = '// This is a comment'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('// This is a comment'); + }); + + it('should preserve comments inside functions', () => { + const code = ` + function getUserDetails() { + // Get user details from API + const response = pm.response.json(); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + function getUserDetails() { + // Get user details from API + const response = res.getBody(); + } + `); + }); + + it('should preserve comments inside if statements', () => { + const code = ` + if (pm.response.code === 200) { + // Success + console.log("Success"); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + if (res.getStatus() === 200) { + // Success + console.log("Success"); + } + `); + }); + + it('should preserve multiline comments', () => { + const code = ` + /* + This is a multiline comment + */ + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + /* + This is a multiline comment + */ + `); + }); + + it('should preserve comments inside for loops', () => { + const code = ` + for (let i = 0; i < 10; i++) { + // Loop iteration + console.log(pm.response.json()[i]); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + for (let i = 0; i < 10; i++) { + // Loop iteration + console.log(res.getBody()[i]); + } + `); + }); + + // Multiple transformations in the same code block + it('should handle multiple translations in the same code block', () => { + const code = ` + const token = pm.environment.get("authToken"); + pm.test("Auth flow works", function() { + const response = pm.response.json(); + pm.expect(response.authenticated).to.be.true; + pm.environment.set("userId", response.user.id); + pm.collectionVariables.set("sessionId", response.session.id); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).not.toContain('pm.test("Auth flow works", function() {'); + expect(translatedCode).not.toContain('pm.expect(response.authenticated).to.be.true;'); + expect(translatedCode).not.toContain('pm.environment.set("userId", response.user.id);'); + expect(translatedCode).not.toContain('pm.collectionVariables.set("sessionId", response.session.id);'); + expect(translatedCode).toContain('const token = bru.getEnvVar("authToken");'); + expect(translatedCode).toContain('test("Auth flow works", function() {'); + expect(translatedCode).toContain('const response = res.getBody();'); + expect(translatedCode).toContain('expect(response.authenticated).to.be.true;'); + expect(translatedCode).toContain('bru.setEnvVar("userId", response.user.id);'); + expect(translatedCode).toContain('bru.setVar("sessionId", response.session.id);'); + }); + + // Nested expressions + it('should handle nested Postman API calls', () => { + const code = 'pm.environment.set("computed", pm.variables.get("base") + "-suffix");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setEnvVar("computed", bru.getVar("base") + "-suffix");'); + }); + + it('should handle more complex nested expressions', () => { + const code = 'pm.collectionVariables.set("fullPath", pm.environment.get("baseUrl") + pm.variables.get("endpoint"));'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setVar("fullPath", bru.getEnvVar("baseUrl") + bru.getVar("endpoint"));'); + }); + + // Unrelated code + it('should leave unrelated code untouched', () => { + const code = ` + function calculateTotal(items) { + return items.reduce((sum, item) => sum + item.price, 0); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(code); + }); + + it('should handle Postman API calls within JavaScript methods', () => { + const code = ` + const helpers = { + getAuthHeader: function() { + return "Bearer " + pm.environment.get("token"); + } + }; + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('return "Bearer " + bru.getEnvVar("token");'); + }); + + + it('should handle aliases with object destructuring', () => { + const code = ` + const { environment, variables } = pm; + environment.set("token", "abc123"); + variables.get("userId"); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toBe(` + bru.setEnvVar("token", "abc123"); + bru.getVar("userId"); + `); + }); + + // Code context tests + it('should translate pm commands inside functions', () => { + const code = ` + function getAuthHeader() { + return "Bearer " + pm.environment.get("token"); + } + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + function getAuthHeader() { + return "Bearer " + bru.getEnvVar("token"); + } + `); + }); + + it('should translate pm commands inside if statements', () => { + const code = ` + if (pm.response.code === 200) { + console.log("Success"); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + if (res.getStatus() === 200) { + console.log("Success"); + } + `); + }); + + + it('should translate pm commands inside if statements', () => { + const code = ` + const json = pm.response.json(); + if (json.code === 200) { + console.log("Success"); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const json = res.getBody(); + if (json.code === 200) { + console.log("Success"); + } + `); + }); + + it('should translate pm commands inside else statements', () => { + const code = ` + if (pm.response.code === 200) { + console.log("Success"); + pm.response.to.have.status(200); + } else { + console.log("Failure"); + expect(res.getStatus()).to.equal(400); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + if (res.getStatus() === 200) { + console.log("Success"); + expect(res.getStatus()).to.equal(200); + } else { + console.log("Failure"); + expect(res.getStatus()).to.equal(400); + } + `); + }); + + it('should translate pm commands inside for loops', () => { + const code = ` + for (let i = 0; i < pm.response.json().length; i++) { + console.log(pm.response.json()[i]); + } + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + for (let i = 0; i < res.getBody().length; i++) { + console.log(res.getBody()[i]); + } + `); + }); + + it('should translate pm commands inside while loops', () => { + const code = ` + while (pm.response.code === 200) { + console.log("Success"); + } + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + while (res.getStatus() === 200) { + console.log("Success"); + } + `); + }); + + it('should translate pm commands inside switch statements', () => { + const code = ` + switch (pm.response.code) { + case 200: + console.log("Success"); + break; + } + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + switch (res.getStatus()) { + case 200: + console.log("Success"); + break; + } + `); + }); + + it('should translate pm commands inside try catch statements', () => { + const code = ` + try { + pm.response.to.have.status(200); + } catch (error) { + console.log("Failure"); + expect(res.getStatus()).to.equal(400); + } + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + try { + expect(res.getStatus()).to.equal(200); + } catch (error) { + console.log("Failure"); + expect(res.getStatus()).to.equal(400); + } + `); + }); + + it('should translate aliases within if statements block', () => { + const code = ` + const env = pm.environment; + const vars = pm.variables; + const collVars = pm.collectionVariables; + const test = pm.test; + const expect = pm.expect; + const response = pm.response; + + function processResponse() { + if(response.code === 200) { + console.log("Success"); + } else if(response.code === 400) { + console.log("Failure"); + expect(response.code).to.equal(400); + } else { + console.log("Unknown status code"); + expect(response.code).to.equal(500); + } + } + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + function processResponse() { + if(res.getStatus() === 200) { + console.log("Success"); + } else if(res.getStatus() === 400) { + console.log("Failure"); + expect(res.getStatus()).to.equal(400); + } else { + console.log("Unknown status code"); + expect(res.getStatus()).to.equal(500); + } + } + `); + }); + + it('should handle pm aliases inside functions', () => { + const code = ` + const tempRes = pm.response; + const tempTest = pm.test; + const tempExpect = pm.expect; + const tempEnv = pm.environment; + const tempVars = pm.variables; + const tempCollVars = pm.collectionVariables; + + function processResponse() { + tempTest("Status code is 200", function() { expect(tempRes.code).to.equal(200); }); + tempEnv.set("userId", tempRes.json().userId); + tempVars.set("token", tempRes.json().token); + tempCollVars.set("sessionId", tempRes.json().sessionId); + } + `; + + const translatedCode = translateCode(code); + + expect(translatedCode).toBe(` + function processResponse() { + test("Status code is 200", function() { expect(res.getStatus()).to.equal(200); }); + bru.setEnvVar("userId", res.getBody().userId); + bru.setVar("token", res.getBody().token); + bru.setVar("sessionId", res.getBody().sessionId); + } + `); + }); + + it('should nested pm commands', () => { + const code = ` + pm.collectionVariables.get(pm.environment.get('key')) + pm.test("Status code is 200", function() { + pm.response.to.have.status(200); + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.getVar(bru.getEnvVar('key')) + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); + `); + }); + + it('should handle pm objects in template literals', () => { + const code = ` + const baseUrl = pm.environment.get("baseUrl"); + const endpoint = pm.variables.get("endpoint"); + const url = \`\${baseUrl}/api/\${endpoint}\`; + console.log(\`Response status: \${pm.response.code}\`); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const baseUrl = bru.getEnvVar("baseUrl");'); + expect(translatedCode).toContain('const endpoint = bru.getVar("endpoint");'); + expect(translatedCode).toContain('const url = `${baseUrl}/api/${endpoint}`;'); + expect(translatedCode).toContain('console.log(`Response status: ${res.getStatus()}`);'); + }); + + it('should handle pm objects in arrow functions', () => { + const code = ` + const getAuthHeader = () => "Bearer " + pm.environment.get("token"); + const processItems = items => items.forEach(item => { + pm.variables.set(item.key, item.value); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const getAuthHeader = () => "Bearer " + bru.getEnvVar("token");'); + expect(translatedCode).toContain('const processItems = items => items.forEach(item => {'); + expect(translatedCode).toContain('bru.setVar(item.key, item.value);'); + }); + + it('test', () => { + const code = ` + const globals = pm.globals; + const key = globals.get("key"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const globals = pm.globals; + const key = globals.get("key"); + `); + }) +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/environment.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/environment.test.js new file mode 100644 index 000000000..c3461f6de --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/environment.test.js @@ -0,0 +1,242 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Environment Variable Translation', () => { + it('should translate pm.environment.get', () => { + const code = 'pm.environment.get("test");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.getEnvVar("test");'); + }); + + it('should translate pm.environment.set', () => { + const code = 'pm.environment.set("test", "value");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setEnvVar("test", "value");'); + }); + + it('should translate pm.environment.has', () => { + const code = 'pm.environment.has("test")'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.getEnvVar("test") !== undefined && bru.getEnvVar("test") !== null'); + }); + + it('should translate pm.environment.unset', () => { + const code = 'pm.environment.unset("test");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.deleteEnvVar("test");'); + }); + + it('should translate pm.environment.name', () => { + const code = 'pm.environment.name;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.getEnvName();'); + }); + + it('should handle nested Postman API calls with environment', () => { + const code = 'pm.environment.set("computed", pm.variables.get("base") + "-suffix");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setEnvVar("computed", bru.getVar("base") + "-suffix");'); + }); + + it('should handle JSON operations with environment variables', () => { + const code = 'pm.environment.set("user", JSON.stringify({ id: 123, name: "John" }));'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setEnvVar("user", JSON.stringify({ id: 123, name: "John" }));'); + }); + + it('should handle JSON.parse with environment variables', () => { + const code = 'const userData = JSON.parse(pm.environment.get("user"));'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const userData = JSON.parse(bru.getEnvVar("user"));'); + }); + + it('should translate pm.environment.name with different access patterns', () => { + const code = ` + const envName1 = pm.environment.name; + const env = pm.environment; + const envName2 = env.name; + console.log(pm.environment.name); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const envName1 = bru.getEnvName(); + const envName2 = bru.getEnvName(); + console.log(bru.getEnvName()); + `); + }); + + it('should handle environment aliases', () => { + const code = ` + const env = pm.environment; + const name = env.name; + const has = env.has("test"); + const set = env.set("test", "value"); + const get = env.get("test"); + const unset = env.unset("test"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const name = bru.getEnvName(); + const has = bru.getEnvVar("test") !== undefined && bru.getEnvVar("test") !== null; + const set = bru.setEnvVar("test", "value"); + const get = bru.getEnvVar("test"); + const unset = bru.deleteEnvVar("test"); + `); + }); + + // Legacy API (postman.) tests related to environment + it('should translate postman.setEnvironmentVariable', () => { + const code = 'postman.setEnvironmentVariable("apiKey", "abc123");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setEnvVar("apiKey", "abc123");'); + }); + + it('should translate postman.getEnvironmentVariable', () => { + const code = 'const baseUrl = postman.getEnvironmentVariable("baseUrl");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const baseUrl = bru.getEnvVar("baseUrl");'); + }); + + it('should translate postman.clearEnvironmentVariable', () => { + const code = 'postman.clearEnvironmentVariable("tempToken");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.deleteEnvVar("tempToken");'); + }); + + it('should handle all environment variable methods together', () => { + const code = ` + // All environment variable methods + const envName = pm.environment.name; + const hasToken = pm.environment.has("token"); + const token = pm.environment.get("token"); + pm.environment.set("timestamp", new Date().toISOString()); + + console.log(\`Environment: \${envName}, Has token: \${hasToken}, Token: \${token}\`); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const envName = bru.getEnvName();'); + expect(translatedCode).toContain('const hasToken = bru.getEnvVar("token") !== undefined && bru.getEnvVar("token") !== null;'); + expect(translatedCode).toContain('const token = bru.getEnvVar("token");'); + expect(translatedCode).toContain('bru.setEnvVar("timestamp", new Date().toISOString());'); + }); + + // Additional robust tests for environment variables + it('should handle environment variables with computed property names', () => { + const code = ` + const prefix = "api"; + const suffix = "Key"; + pm.environment.set(prefix + "_" + suffix, "abc123"); + const computedValue = pm.environment.get(prefix + "_" + suffix); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('bru.setEnvVar(prefix + "_" + suffix, "abc123");'); + expect(translatedCode).toContain('const computedValue = bru.getEnvVar(prefix + "_" + suffix);'); + }); + + it('should handle environment variables in complex object structures', () => { + const code = ` + const config = { + baseUrl: pm.environment.get("apiUrl"), + headers: { + "Authorization": "Bearer " + pm.environment.get("token"), + "X-Api-Key": pm.environment.get("apiKey") || "default-key" + }, + timeout: parseInt(pm.environment.get("timeout") || "5000"), + validate: pm.environment.has("validateResponses") + }; + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('baseUrl: bru.getEnvVar("apiUrl"),'); + expect(translatedCode).toContain('"Authorization": "Bearer " + bru.getEnvVar("token"),'); + expect(translatedCode).toContain('"X-Api-Key": bru.getEnvVar("apiKey") || "default-key"'); + expect(translatedCode).toContain('timeout: parseInt(bru.getEnvVar("timeout") || "5000"),'); + expect(translatedCode).toContain('validate: bru.getEnvVar("validateResponses") !== undefined && bru.getEnvVar("validateResponses") !== null'); + }); + + it('should handle environment variables in conditionals correctly', () => { + const code = ` + if (pm.environment.has("apiKey")) { + if (pm.environment.get("apiKey").length > 0) { + console.log("Valid API key exists"); + } else { + console.log("API key is empty"); + } + } else { + console.log("No API key defined"); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('if (bru.getEnvVar("apiKey") !== undefined && bru.getEnvVar("apiKey") !== null) {'); + expect(translatedCode).toContain('if (bru.getEnvVar("apiKey").length > 0) {'); + }); + + it('should handle multiple levels of environment variable aliasing', () => { + const code = ` + const env = pm.environment; + + env.set("key", "value"); + const value = env.get("key"); + const exists = env.has("key"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.setEnvVar("key", "value"); + const value = bru.getEnvVar("key"); + const exists = bru.getEnvVar("key") !== undefined && bru.getEnvVar("key") !== null; + `); + }); + + it('should handle environment variables with dynamic values', () => { + const code = ` + // Generate a timestamp for this request + const timestamp = new Date().toISOString(); + pm.environment.set("requestTimestamp", timestamp); + + // Generate a unique ID + const uniqueId = "req_" + Math.random().toString(36).substring(2, 15); + pm.environment.set("requestId", uniqueId); + + // Calculate an expiry time (30 minutes from now) + const expiryTime = new Date(); + expiryTime.setMinutes(expiryTime.getMinutes() + 30); + pm.environment.set("tokenExpiry", expiryTime.getTime()); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('bru.setEnvVar("requestTimestamp", timestamp);'); + expect(translatedCode).toContain('bru.setEnvVar("requestId", uniqueId);'); + expect(translatedCode).toContain('bru.setEnvVar("tokenExpiry", expiryTime.getTime());'); + }); + + it('should handle environment variables in try-catch blocks', () => { + const code = ` + try { + const configStr = pm.environment.get("config"); + const config = JSON.parse(configStr); + console.log("Config loaded:", config.version); + } catch (error) { + console.error("Failed to parse config"); + pm.environment.set("configError", error.message); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const configStr = bru.getEnvVar("config");'); + expect(translatedCode).toContain('bru.setEnvVar("configError", error.message);'); + }); + + it('should handle legacy environment and pm.setEnvironmentVariable together', () => { + const code = ` + // Legacy style + postman.setEnvironmentVariable("legacyKey", "legacyValue"); + + // Mixed with newer style + const value = pm.environment.get("anotherKey"); + + // Another legacy form + pm.setEnvironmentVariable("thirdKey", "thirdValue"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('bru.setEnvVar("legacyKey", "legacyValue");'); + expect(translatedCode).toContain('const value = bru.getEnvVar("anotherKey");'); + expect(translatedCode).toContain('bru.setEnvVar("thirdKey", "thirdValue");'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/exec-flow.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/exec-flow.test.js new file mode 100644 index 000000000..053e99685 --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/exec-flow.test.js @@ -0,0 +1,64 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Execution Flow Translation', () => { + // Request flow control + it('should translate pm.setNextRequest', () => { + const code = 'pm.setNextRequest("Get User Details");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setNextRequest("Get User Details");'); + }); + + it('should translate pm.execution.skipRequest', () => { + const code = 'if (condition) pm.execution.skipRequest();'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('if (condition) bru.runner.skipRequest();'); + }); + + it('should translate pm.execution.setNextRequest(null)', () => { + const code = 'pm.execution.setNextRequest(null);'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.runner.stopExecution();'); + }); + + it('should translate pm.execution.setNextRequest("null")', () => { + const code = 'pm.execution.setNextRequest("null");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.runner.stopExecution();'); + }); + + it('should handle pm.execution.setNextRequest with non-null parameters', () => { + const code = ` + // Continue normal flow + pm.execution.setNextRequest("Get user details"); + + // With variable + const nextReq = "Update profile"; + pm.execution.setNextRequest(nextReq); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('bru.runner.setNextRequest("Get user details");'); + expect(translatedCode).toContain('bru.runner.setNextRequest(nextReq);'); + }); + + it('should handle all execution control methods together', () => { + const code = ` + // All execution control methods + if (pm.response.code === 401) { + pm.execution.skipRequest(); + } else if (pm.response.code === 500) { + pm.execution.setNextRequest(null); + } else { + pm.setNextRequest("Get User Details"); + } + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('if (res.getStatus() === 401) {'); + expect(translatedCode).toContain('bru.runner.skipRequest();'); + expect(translatedCode).toContain('} else if (res.getStatus() === 500) {'); + expect(translatedCode).toContain('bru.runner.stopExecution();'); + expect(translatedCode).toContain('} else {'); + expect(translatedCode).toContain('bru.setNextRequest("Get User Details");'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/legacy-tests-syntax.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/legacy-tests-syntax.test.js new file mode 100644 index 000000000..e548aa03c --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/legacy-tests-syntax.test.js @@ -0,0 +1,283 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Legacy Tests[] Syntax Translation', () => { + it('should handle tests[] commands', () => { + const code = ` + tests["Status code is 200"] = pm.response.code === 200;`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Status code is 200", function() { + expect(Boolean(res.getStatus() === 200)).to.be.true; + });`); + }); + + it('should handle tests[] with complex expressions', () => { + const code = ` + tests["Response has valid data"] = pm.response.json().data && pm.response.json().data.length > 0;`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Response has valid data", function() { + expect(Boolean(res.getBody().data && res.getBody().data.length > 0)).to.be.true; + });`); + }); + + it('should handle tests[] with string equality', () => { + const code = ` + tests["Content-Type is application/json"] = pm.response.headers.get("Content-Type") === "application/json";`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Content-Type is application/json", function() { + expect(Boolean(res.getHeaders().get("Content-Type") === "application/json")).to.be.true; + });`); + }); + + it('should handle tests[] with function calls', () => { + const code = ` + tests["Response time is acceptable"] = pm.response.responseTime < 500;`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Response time is acceptable", function() { + expect(Boolean(res.getResponseTime() < 500)).to.be.true; + });`); + }); + + it('should handle tests[] with variable references', () => { + const code = ` + const expectedStatus = 201; + tests["Status code is correct"] = pm.response.code === expectedStatus;`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const expectedStatus = 201; + test("Status code is correct", function() { + expect(Boolean(res.getStatus() === expectedStatus)).to.be.true; + });`); + }); + + it('should handle multiple tests[] statements', () => { + const code = ` + tests["Status code is 200"] = pm.response.code === 200; + tests["Response has data"] = pm.response.json().hasOwnProperty("data");`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Status code is 200", function() { + expect(Boolean(res.getStatus() === 200)).to.be.true; + }); + test("Response has data", function() { + expect(Boolean(res.getBody().hasOwnProperty("data"))).to.be.true; + });`); + }); + + it('should handle tests[] with special characters in name', () => { + const code = ` + tests["Special characters: !@#$%^&*()"] = true;`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Special characters: !@#$%^&*()", function() { + expect(Boolean(true)).to.be.true; + });`); + }); + + it('should handle tests[] with pm.environment variables', () => { + const code = ` + tests["Response matches environment variable"] = pm.response.json().id === pm.environment.get("expectedId");`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Response matches environment variable", function() { + expect(Boolean(res.getBody().id === bru.getEnvVar("expectedId"))).to.be.true; + });`); + }); + + it('should handle nested pm objects in tests[] assignments', () => { + const code = ` + tests["Authentication header is present"] = pm.request.headers.has("Authorization"); + tests["Data count is correct"] = pm.response.json().items.length === pm.variables.get("expectedCount"); + `; + const translatedCode = translateCode(code); + + // The exact translation might vary depending on implementation details, + // but we can check for key transformations + expect(translatedCode).toContain('test("Authentication header is present"'); + expect(translatedCode).toContain('test("Data count is correct"'); + expect(translatedCode).toContain('res.getBody().items.length === bru.getVar("expectedCount")'); + }); + + // Additional robust tests for legacy tests[] syntax + it('should handle tests[] with complex boolean expressions', () => { + const code = ` + tests["Complex validation"] = (pm.response.code >= 200 && pm.response.code < 300) || + (pm.response.json().success === true && pm.response.json().data !== null);`; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Complex validation", function() {'); + expect(translatedCode).toContain('expect(Boolean((res.getStatus() >= 200 && res.getStatus() < 300) ||'); + expect(translatedCode).toContain('(res.getBody().success === true && res.getBody().data !== null))).to.be.true;'); + }); + + it('should handle tests[] with array methods', () => { + const code = ` + tests["All items have an ID"] = pm.response.json().items.every(item => item.hasOwnProperty('id')); + tests["Has premium item"] = pm.response.json().items.some(item => item.type === 'premium');`; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("All items have an ID", function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getBody().items.every(item => item.hasOwnProperty(\'id\')))).to.be.true;'); + expect(translatedCode).toContain('test("Has premium item", function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getBody().items.some(item => item.type === \'premium\'))).to.be.true;'); + }); + + it('should handle tests[] with template literals in the name', () => { + const code = ` + const endpoint = "users"; + tests[\`Endpoint \${endpoint} returns valid response\`] = pm.response.code === 200;`; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const endpoint = "users";'); + expect(translatedCode).toContain('test(`Endpoint ${endpoint} returns valid response`, function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getStatus() === 200)).to.be.true;'); + }); + + it('should handle tests[] with deep property access', () => { + const code = ` + tests["User has admin role"] = pm.response.json().user && + pm.response.json().user.roles && + pm.response.json().user.roles.includes('admin');`; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("User has admin role", function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getBody().user &&'); + expect(translatedCode).toContain('res.getBody().user.roles &&'); + expect(translatedCode).toContain('res.getBody().user.roles.includes(\'admin\'))).to.be.true;'); + }); + + it('should handle tests[] with JSON schema validation patterns', () => { + const code = ` + const schema = { + type: "object", + required: ["id", "name"], + properties: { + id: { type: "string" }, + name: { type: "string" } + } + }; + + const data = pm.response.json(); + + // Basic schema validation patterns + tests["Has required fields"] = data.hasOwnProperty('id') && data.hasOwnProperty('name'); + tests["ID is string"] = typeof data.id === 'string'; + tests["Name is string"] = typeof data.name === 'string';`; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const schema = {'); + expect(translatedCode).toContain('type: "object",'); + expect(translatedCode).toContain('required: ["id", "name"],'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('test("Has required fields", function() {'); + expect(translatedCode).toContain('expect(Boolean(data.hasOwnProperty(\'id\') && data.hasOwnProperty(\'name\'))).to.be.true;'); + expect(translatedCode).toContain('test("ID is string", function() {'); + expect(translatedCode).toContain('expect(Boolean(typeof data.id === \'string\')).to.be.true;'); + }); + + it('should handle tests[] within conditional blocks', () => { + const code = ` + const data = pm.response.json(); + + if (pm.response.code === 200) { + tests["Success response has data"] = data.hasOwnProperty('items'); + + if (data.items.length > 0) { + tests["First item has ID"] = data.items[0].hasOwnProperty('id'); + } + } else { + tests["Error response has message"] = data.hasOwnProperty('message'); + }`; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('if (res.getStatus() === 200) {'); + expect(translatedCode).toContain('test("Success response has data", function() {'); + expect(translatedCode).toContain('expect(Boolean(data.hasOwnProperty(\'items\'))).to.be.true;'); + expect(translatedCode).toContain('if (data.items.length > 0) {'); + expect(translatedCode).toContain('test("First item has ID", function() {'); + expect(translatedCode).toContain('expect(Boolean(data.items[0].hasOwnProperty(\'id\'))).to.be.true;'); + expect(translatedCode).toContain('} else {'); + expect(translatedCode).toContain('test("Error response has message", function() {'); + expect(translatedCode).toContain('expect(Boolean(data.hasOwnProperty(\'message\'))).to.be.true;'); + }); + + it('should handle tests[] with combination of legacy and modern styles', () => { + const code = ` + // Legacy style + tests["Status code is 200"] = pm.response.code === 200; + + // Modern style + pm.test("Response has valid data", function() { + const json = pm.response.json(); + pm.expect(json).to.be.an('object'); + pm.expect(json.items).to.be.an('array'); + + // Mix by using tests[] inside pm.test + tests["All items have price"] = json.items.every(item => item.hasOwnProperty('price')); + });`; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Status code is 200", function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getStatus() === 200)).to.be.true;'); + expect(translatedCode).toContain('test("Response has valid data", function() {'); + expect(translatedCode).toContain('const json = res.getBody();'); + expect(translatedCode).toContain('expect(json).to.be.an(\'object\');'); + expect(translatedCode).toContain('expect(json.items).to.be.an(\'array\');'); + expect(translatedCode).toContain('test("All items have price", function() {'); + expect(translatedCode).toContain('expect(Boolean(json.items.every(item => item.hasOwnProperty(\'price\')))).to.be.true;'); + }); + + it('should handle complex real-world tests[] example', () => { + const code = ` + // Parse response + const response = pm.response.json(); + + // Basic response validation + tests["Status code is 200"] = pm.response.code === 200; + tests["Response is valid JSON"] = response !== null && typeof response === 'object'; + + // Check headers + tests["Has content-type header"] = pm.response.headers.has("Content-Type"); + tests["Content-Type is JSON"] = pm.response.headers.get("Content-Type").includes("application/json"); + + // Validate against expected values + const expectedItems = parseInt(pm.environment.get("expectedItemCount")); + tests["Has correct number of items"] = response.items.length === expectedItems; + + // Check for required fields on all items + const requiredFields = ["id", "name", "price", "category"]; + tests["All items have required fields"] = response.items.every(item => { + return requiredFields.every(field => item.hasOwnProperty(field)); + }); + + // Validate specific business rules + tests["No items with zero price"] = response.items.every(item => parseFloat(item.price) > 0); + tests["Has at least one featured item"] = response.items.some(item => item.featured === true); + + // If we find a specific item we're looking for, save its ID for later + const targetItem = response.items.find(item => item.name === pm.variables.get("targetItemName")); + if (targetItem) { + pm.environment.set("targetItemId", targetItem.id); + tests["Found target item"] = true; + }`; + const translatedCode = translateCode(code); + + // Check key transformations + expect(translatedCode).toContain('const response = res.getBody();'); + expect(translatedCode).toContain('test("Status code is 200", function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getStatus() === 200)).to.be.true;'); + expect(translatedCode).toContain('test("Has content-type header", function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getHeaders().has("Content-Type"))).to.be.true;'); + expect(translatedCode).toContain('test("Content-Type is JSON", function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getHeaders().get("Content-Type").includes("application/json"))).to.be.true;'); + expect(translatedCode).toContain('const expectedItems = parseInt(bru.getEnvVar("expectedItemCount"));'); + expect(translatedCode).toContain('test("Has correct number of items", function() {'); + expect(translatedCode).toContain('expect(Boolean(response.items.length === expectedItems)).to.be.true;'); + expect(translatedCode).toContain('const targetItem = response.items.find(item => item.name === bru.getVar("targetItemName"));'); + expect(translatedCode).toContain('bru.setEnvVar("targetItemId", targetItem.id);'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/multiline-syntax.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/multiline-syntax.test.js new file mode 100644 index 000000000..a9be82130 --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/multiline-syntax.test.js @@ -0,0 +1,283 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Multiline Syntax Handling', () => { + it('should handle basic multiline variable syntax with indentation', () => { + const code = ` + const userId = pm.variables + .get("userId"); + pm.variables + .set("timestamp", new Date().toISOString()); + const hasToken = pm.variables + .has("token"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const userId = bru.getVar("userId"); + bru.setVar("timestamp", new Date().toISOString()); + const hasToken = bru.hasVar("token"); + `); + }); + + it('should handle multiline environment variable syntax', () => { + const code = ` + const baseUrl = pm + .environment + .get("baseUrl"); + pm + .environment + .set("requestTime", Date.now()); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const baseUrl = bru.getEnvVar("baseUrl"); + bru.setEnvVar("requestTime", Date.now()); + `); + }); + + it('should handle multiline collection variable syntax', () => { + const code = ` + const apiKey = pm.collectionVariables + .get("apiKey"); + pm.collectionVariables + .set("lastRun", new Date().toISOString()); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const apiKey = bru.getVar("apiKey"); + bru.setVar("lastRun", new Date().toISOString()); + `); + }); + + it('should handle complex environment.has transformation with multiline syntax', () => { + const code = ` + if (pm.environment + .has("apiKey")) { + console.log("API Key exists"); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + if (bru.getEnvVar("apiKey") !== undefined && bru.getEnvVar("apiKey") !== null) { + console.log("API Key exists"); + } + `); + }); + + it('should handle response.to.have.status with multiline formatting', () => { + const code = ` + pm.test("Status code is correct", function() { + pm + .response + .to + .have + .status(200); + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200)'); + }); + + it('should handle response.to.have.header with multiline formatting', () => { + const code = ` + pm.test("Content type is present", function() { + pm + .response + .to + .have + .header("content-type"); + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("content-type".toLowerCase())'); + }); + + it('should handle response properties with multiline syntax', () => { + const code = ` + const responseBody = pm + .response + .json(); + const responseText = pm + .response + .text; + const responseTime = pm + .response + .responseTime; + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const responseBody = res.getBody()'); + expect(translatedCode).toContain('const responseText = '); + expect(translatedCode).toContain('const responseTime = res.getResponseTime()'); + }); + + it('should handle execution flow control with multiline syntax', () => { + const code = ` + // Stop execution + pm + .execution + .setNextRequest(null); + + // Continue to next request + pm + .execution + .setNextRequest("Next API Call"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('// Stop execution'); + expect(translatedCode).toContain('// Continue to next request'); + expect(translatedCode).toContain('bru.runner.stopExecution()'); + expect(translatedCode).toContain('bru.runner.setNextRequest("Next API Call")'); + }); + + it('should handle mixed normal and multiline syntax in the same code', () => { + const code = ` + // Normal syntax + const normalVar = pm.variables.get("normal"); + + // Multiline syntax + const multilineVar = pm.variables + .get("multiline"); + + // Normal syntax again + pm.variables.set("normalSet", "value"); + + // Multiline syntax again + pm.variables + .set("multilineSet", "value"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + // Normal syntax + const normalVar = bru.getVar("normal"); + + // Multiline syntax + const multilineVar = bru.getVar("multiline"); + + // Normal syntax again + bru.setVar("normalSet", "value"); + + // Multiline syntax again + bru.setVar("multilineSet", "value"); + `); + }); + + it('should handle complex multiline method chaining', () => { + const code = ` + pm + .test("Test with chaining", function() { + pm + .response + .to + .have + .status(200); + + const body = pm + .response + .json(); + + pm + .expect(body) + .to + .have + .property('success') + .equal(true); + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('test("Test with chaining", function() {'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200)'); + expect(translatedCode).toContain('const body = res.getBody()'); + expect(translatedCode).toContain('.property(\'success\')'); + expect(translatedCode).toContain('.equal(true)'); + }); + + it('should handle a comprehensive script with various multiline formats', () => { + const code = ` + // This comprehensive script tests different multiline styles and whitespace variations + + // Environment variables with different formatting styles + const baseUrl = pm.environment.get("baseUrl"); + const apiKey = pm + .environment + .get("apiKey"); + const userId = pm.environment + .get("userId"); + + // Mix of variable styles + pm.variables.set("testId", "test-" + Date.now()); + pm + .variables + .set("timestamp", new Date().toISOString()); + + // Collection variables with inconsistent spacing + pm.collectionVariables + .set("lastRun", new Date()); + + // Complex conditionals with multiline expressions + if (pm + .environment + .has("apiKey") && + pm.variables.has("testId")) { + + // Testing response with mixed syntax styles + pm.test("Response validation", function() { + // Normal style + pm.response.to.have.status(200); + + // Multiline with different indentation + pm + .response + .to + .have + .header("content-type"); + + pm.response + .to.have + .jsonBody("success", true); + + // Extreme indentation + pm + .response + .to + .not + .have + .jsonBody("error"); + }); + + // Flow control with mixed styles + if (pm.response.code === 401) { + pm.execution.setNextRequest(null); + } else { + pm + .execution + .setNextRequest("Next API Call"); + } + } + `; + + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const baseUrl = bru.getEnvVar("baseUrl")'); + expect(translatedCode).toContain('const apiKey = bru.getEnvVar("apiKey")'); + expect(translatedCode).toContain('const userId = bru.getEnvVar("userId")'); + + // Check variables translations + expect(translatedCode).toContain('bru.setVar("testId", "test-" + Date.now())'); + expect(translatedCode).toContain('bru.setVar("timestamp", new Date().toISOString())'); + + // Check collection variables + expect(translatedCode).toContain('bru.setVar("lastRun", new Date())'); + + // Check complex conditionals + expect(translatedCode).toContain('if (bru.getEnvVar("apiKey") !== undefined && bru.getEnvVar("apiKey") !== null &&'); + expect(translatedCode).toContain('bru.hasVar("testId"))'); + + // Check response testing + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200)'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("content-type".toLowerCase())'); + + // Check flow control + expect(translatedCode).toContain('if (res.getStatus() === 401)'); + expect(translatedCode).toContain('bru.runner.stopExecution()'); + expect(translatedCode).toContain('bru.runner.setNextRequest("Next API Call")'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/postman-references.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/postman-references.test.js new file mode 100644 index 000000000..20e7890a7 --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/postman-references.test.js @@ -0,0 +1,132 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Postman to PM References Conversion', () => { + // Basic conversions + it('should convert basic postman references to pm', () => { + const code = 'postman.setEnvironmentVariable("key", "value");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setEnvVar("key", "value");'); + // The key part is that it should convert postman.* to pm.* internally before + // translating to bru.* APIs + }); + + it('should convert postman variable access to pm', () => { + const code = 'const value = postman.getEnvironmentVariable("key");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const value = bru.getEnvVar("key");'); + }); + + it('should handle postman variable assignments', () => { + const code = ` + const envVar = postman.environment.get("apiKey"); + const baseUrl = postman.environment.get("baseUrl"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const envVar = bru.getEnvVar("apiKey");'); + expect(translatedCode).toContain('const baseUrl = bru.getEnvVar("baseUrl");'); + }); + + // More complex patterns + it('should handle mixed postman and pm references in the same code', () => { + const code = ` + // Using both postman and pm APIs + const apiKey = postman.environment.get("apiKey"); + const baseUrl = pm.environment.get("baseUrl"); + + // Using both formats in a test + postman.test("Status code is 200", function() { + pm.expect(pm.response.code).to.equal(200); + }); + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const apiKey = bru.getEnvVar("apiKey");'); + expect(translatedCode).toContain('const baseUrl = bru.getEnvVar("baseUrl");'); + expect(translatedCode).toContain('test("Status code is 200", function() {'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);'); + }); + + it('should handle postman references in object destructuring', () => { + const code = ` + const { environment } = postman; + environment.set("key", "value"); + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toContain('bru.setEnvVar("key", "value");'); + }); + + // Complex control flows + it('should handle postman references in control flow statements', () => { + const code = ` + if (postman.environment.get("isProduction") === "true") { + const apiUrl = postman.environment.get("prodUrl"); + postman.setNextRequest("Production Flow"); + } else { + const apiUrl = postman.environment.get("devUrl"); + postman.setNextRequest("Development Flow"); + } + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toContain('if (bru.getEnvVar("isProduction") === "true") {'); + expect(translatedCode).toContain('const apiUrl = bru.getEnvVar("prodUrl");'); + expect(translatedCode).toContain('bru.setNextRequest("Production Flow");'); + expect(translatedCode).toContain('const apiUrl = bru.getEnvVar("devUrl");'); + expect(translatedCode).toContain('bru.setNextRequest("Development Flow");'); + }); + + // Legacy response handling + it('should handle legacy postman response methods', () => { + const code = ` + // Using legacy response handling + const responseCode = postman.response.code; + const responseBody = postman.response.json(); + + // Set environment variables with response data + postman.setEnvironmentVariable("lastResponseCode", responseCode); + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const responseCode = res.getStatus();'); + expect(translatedCode).toContain('const responseBody = res.getBody();'); + expect(translatedCode).toContain('bru.setEnvVar("lastResponseCode", responseCode);'); + }); + + // Postman in string literals should be untouched + it('should not convert postman references in string literals', () => { + const code = ` + console.log("This is a pm script"); + const message = "We're using pm to test our API"; + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toContain('console.log("This is a pm script");'); + expect(translatedCode).toContain('const message = "We\'re using pm to test our API";'); + }); + + // Complex example with aliasing + it('should handle complex postman reference patterns with aliasing', () => { + const code = ` + // Aliasing the postman object + const env = postman.environment; + const code = postman.code; + + // Using the alias + const apiKey = env.get("apiKey"); + const userId = env.get("userId"); + + // Using alias in tests + postman.test("Response is valid", function() { + postman.expect(code).to.equal(200); + }); + `; + + const translatedCode = translateCode(code); + // Should handle the aliases properly + expect(translatedCode).toContain('const apiKey = bru.getEnvVar("apiKey");'); + expect(translatedCode).toContain('const userId = bru.getEnvVar("userId");'); + expect(translatedCode).toContain('test("Response is valid", function() {'); + expect(translatedCode).toContain('expect(code).to.equal(200);'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/request.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/request.test.js new file mode 100644 index 000000000..a81bc6c72 --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/request.test.js @@ -0,0 +1,108 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Request Translation', () => { + it('should translate pm.request.url', () => { + const code = 'const requestUrl = pm.request.url;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const requestUrl = req.getUrl();'); + }); + + it('should translate pm.request.method', () => { + const code = 'const method = pm.request.method;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const method = req.getMethod();'); + }); + + it('should translate pm.request.headers', () => { + const code = 'const headers = pm.request.headers;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const headers = req.getHeaders();'); + }); + + it('should translate pm.request.body', () => { + const code = 'const body = pm.request.body;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const body = req.getBody();'); + }); + + it('should translate pm.response.statusText', () => { + const code = 'const statusText = pm.response.statusText;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const statusText = res.statusText;'); + }); + + it('should translate multiple request methods in one block', () => { + const code = ` + const url = pm.request.url; + const method = pm.request.method; + const headers = pm.request.headers; + const body = pm.request.body; + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const url = req.getUrl(); + const method = req.getMethod(); + const headers = req.getHeaders(); + const body = req.getBody(); + `); + }); + + it('should handle request and response properties together', () => { + const code = ` + // Get request data + const url = pm.request.url; + const method = pm.request.method; + + // Get response data + const statusCode = pm.response.code; + const statusText = pm.response.statusText; + + // Verify expectations + pm.test("Request was made correctly", function() { + pm.expect(method).to.equal("POST"); + pm.expect(url).to.include("/api/items"); + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const url = req.getUrl();'); + expect(translatedCode).toContain('const method = req.getMethod();'); + expect(translatedCode).toContain('const statusCode = res.getStatus();'); + expect(translatedCode).toContain('const statusText = res.statusText;'); + expect(translatedCode).toContain('test("Request was made correctly", function() {'); + expect(translatedCode).toContain('expect(method).to.equal("POST");'); + expect(translatedCode).toContain('expect(url).to.include("/api/items");'); + }); + + it('should handle request properties in conditional blocks', () => { + const code = ` + if (pm.request.method === "POST") { + console.log("This is a POST request to " + pm.request.url); + pm.test("Request has correct content-type", function() { + pm.expect(pm.request.headers.has("Content-Type")).to.be.true; + }); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('if (req.getMethod() === "POST") {'); + expect(translatedCode).toContain('console.log("This is a POST request to " + req.getUrl());'); + expect(translatedCode).toContain('test("Request has correct content-type", function() {'); + // Note: The expectation for headers.has might be transformed differently + // depending on how complex transformations are handled + }); + + it('should handle request data extraction and variable setting', () => { + const code = ` + // Extract request data + const requestData = pm.request.body; + const contentType = pm.request.headers.get("Content-Type"); + + // Save for later use + pm.variables.set("lastRequestBody", JSON.stringify(requestData)); + pm.environment.set("lastContentType", contentType); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const requestData = req.getBody();'); + expect(translatedCode).toContain('bru.setVar("lastRequestBody", JSON.stringify(requestData));'); + expect(translatedCode).toContain('bru.setEnvVar("lastContentType", contentType);'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/response.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/response.test.js new file mode 100644 index 000000000..7fd4d902b --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/response.test.js @@ -0,0 +1,489 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Response Translation', () => { + // Basic response property tests + it('should translate pm.response.json', () => { + const code = 'const jsonData = pm.response.json();'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const jsonData = res.getBody();'); + }); + + it('should translate pm.response.code', () => { + const code = 'if (pm.response.code === 200) { console.log("Success"); }'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('if (res.getStatus() === 200) { console.log("Success"); }'); + }); + + it('should translate pm.response.text', () => { + const code = 'const responseText = pm.response.text();'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const responseText = JSON.stringify(res.getBody());'); + }); + + it('should translate pm.response.responseTime', () => { + const code = 'console.log("Response time:", pm.response.responseTime);'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('console.log("Response time:", res.getResponseTime());'); + }); + + it('should translate pm.response.statusText', () => { + const code = 'console.log("Status text:", pm.response.statusText);'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('console.log("Status text:", res.statusText);'); + }); + + // Complex response transformations + it('should transform pm.response.to.have.status', () => { + const code = 'pm.response.to.have.status(201);'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('expect(res.getStatus()).to.equal(201);'); + }); + + it('should transform pm.response.to.have.header with single argument', () => { + const code = 'pm.response.to.have.header("Content-Type");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase());'); + }); + + it('should transform multiple pm.response.to.have.header statements', () => { + const code = ` + pm.response.to.have.header("Content-Type", "application/json"); + pm.response.to.have.header("Cache-Control", "no-cache"); + `; + const translatedCode = translateCode(code); + + // Check for the existence of all four assertions (two pairs) + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase(), "application/json");'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Cache-Control".toLowerCase(), "no-cache");'); + }); + + it('should transform pm.response.to.have.header inside control structures', () => { + const code = ` + if (pm.response.code === 200) { + pm.response.to.have.header("Content-Type", "application/json"); + } + `; + const translatedCode = translateCode(code); + + // The assertions should be inside the if block + expect(translatedCode).toContain('if (res.getStatus() === 200) {'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase(), "application/json");'); + }); + + it('should transform pm.response.to.have.header with variable parameters', () => { + const code = ` + const headerName = "Content-Type"; + const expectedValue = "application/json"; + pm.response.to.have.header(headerName, expectedValue); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const headerName = "Content-Type";'); + expect(translatedCode).toContain('const expectedValue = "application/json";'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(headerName.toLowerCase(), expectedValue);'); + }); + + // Response aliases tests + it('should handle response aliases', () => { + const code = ` + const response = pm.response; + const status = response.status; + const body = response.json(); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const status = res.statusText; + const body = res.getBody(); + `); + }); + + // Response to.have.status with different formats + it('should handle pm.response.to.have.status with different status codes', () => { + const code = ` + // Test different status codes + pm.response.to.have.status(200); // OK + pm.response.to.have.status(201); // Created + pm.response.to.have.status(400); // Bad Request + pm.response.to.have.status(404); // Not Found + pm.response.to.have.status(500); // Server Error + + // With variables + const expectedStatus = 200; + pm.response.to.have.status(expectedStatus); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(201);'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(400);'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(404);'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(500);'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(expectedStatus);'); + }); + + // Alias for pm.response.to.have.status + it('should handle pm.response.to.have.status alias', () => { + const code = ` + const resp = pm.response; + resp.to.have.status(200); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + expect(res.getStatus()).to.equal(200); + `); + }); + + it('should handle pm.response.to.have.header alias', () => { + const code = ` + const resp = pm.response; + resp.to.have.header("Content-Type"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase()); + `); + }); + + it('should handle pm.response.to.have.header alias with value check', () => { + const code = ` + const resp = pm.response; + resp.to.have.header("Content-Type", "application/json"); + `; + const translatedCode = translateCode(code); + + // Check for both assertions when using an alias + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase(), "application/json");'); + }); + + + it('should translate response.status', () => { + const code = ` + const resp = pm.response; + const statusCode = resp.status; + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const statusCode = res.statusText; + `); + }); + + it('should translate response.body', () => { + const code = ` + const resp = pm.response; + const responseBody = resp.json(); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const responseBody = res.getBody(); + `); + }); + + it('should translate pm.response.statusText', () => { + const code = ` + const resp = pm.response; + const statusText = resp.statusText; + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const statusText = res.statusText; + `); + }); + + it('should translate multiple response methods in one block', () => { + const code = ` + const resp = pm.response; + const statusCode = resp.code; + const statusText = resp.statusText; + const jsonData = resp.json(); + const responseText = resp.text(); + const time = resp.responseTime; + resp.to.have.status(200); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const statusCode = res.getStatus(); + const statusText = res.statusText; + const jsonData = res.getBody(); + const responseText = JSON.stringify(res.getBody()); + const time = res.getResponseTime(); + expect(res.getStatus()).to.equal(200); + `); + }); + + it('should handle accessing nested properties on response objects', () => { + const code = ` + const resp = pm.response; + const data = resp.json(); + if (data && data.user && data.user.id) { + pm.environment.set("userId", data.user.id); + } + `; + const translatedCode = translateCode(code); + + expect(translatedCode).not.toContain('const resp = pm.response;'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('bru.setEnvVar("userId", data.user.id);'); + }); + + it('should handle all response property methods together', () => { + const code = ` + // All response property methods + const statusCode = pm.response.code; + const responseBody = pm.response.json(); + const responseText = pm.response.text(); + const statusText = pm.response.statusText; + const responseTime = pm.response.responseTime; + + pm.test("Response is valid", function() { + pm.response.to.have.status(200); + pm.expect(responseBody).to.be.an('object'); + pm.expect(responseTime).to.be.below(1000); + pm.expect(statusText).to.equal('OK'); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const statusCode = res.getStatus();'); + expect(translatedCode).toContain('const responseBody = res.getBody();'); + expect(translatedCode).toContain('const responseText = JSON.stringify(res.getBody());'); + expect(translatedCode).toContain('const responseTime = res.getResponseTime();'); + expect(translatedCode).toContain('const statusText = res.statusText;'); + expect(translatedCode).toContain('test("Response is valid", function() {'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);'); + expect(translatedCode).toContain('expect(responseBody).to.be.an(\'object\');'); + expect(translatedCode).toContain('expect(responseTime).to.be.below(1000);'); + expect(translatedCode).toContain('expect(statusText).to.equal(\'OK\');'); + }); + + it('should handle pm objects with array access on response', () => { + const code = ` + const items = pm.response.json().items; + for (let i = 0; i < items.length; i++) { + pm.collectionVariables.set("item_" + i, items[i].id); + } + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const items = res.getBody().items;'); + expect(translatedCode).toContain('bru.setVar("item_" + i, items[i].id);'); + }); + + it('should handle response JSON with optional chaining and nullish coalescing', () => { + const code = ` + const userId = pm.response.json()?.user?.id ?? "anonymous"; + const items = pm.response.json()?.data?.items || []; + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const userId = res.getBody()?.user?.id ?? "anonymous";'); + expect(translatedCode).toContain('const items = res.getBody()?.data?.items || [];'); + }); + + it('should handle response headers with different access patterns', () => { + // will need to handle get, set methods, bruno does not support this yet + const code = ` + const contentType = pm.response.headers.get('Content-Type'); + const contentLength = pm.response.headers.get('Content-Length'); + console.log("contentType", contentType); + console.log("contentLength", contentLength); + + pm.test("Headers are correct", function() { + pm.response.to.have.header('Content-Type'); + pm.response.to.have.header('Content-Length'); + pm.expect(contentType).to.include('application/json'); + }); + `; + const translatedCode = translateCode(code); + + // Check how header access is translated + expect(translatedCode).toContain('const contentType = res.getHeaders().get(\'Content-Type\');'); + expect(translatedCode).toContain('const contentLength = res.getHeaders().get(\'Content-Length\');'); + expect(translatedCode).toContain('console.log("contentType", contentType);'); + expect(translatedCode).toContain('console.log("contentLength", contentLength);'); + expect(translatedCode).not.toContain('pm.test') + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\'Content-Type\'.toLowerCase())'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\'Content-Length\'.toLowerCase())'); + expect(translatedCode).toContain('expect(contentType).to.include(\'application/json\')'); + }); + + it('should transform response data with array destructuring', () => { + const code = ` + const { id, name, items } = pm.response.json(); + const [first, second] = items; + pm.environment.set("userId", id); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const { id, name, items } = res.getBody();'); + expect(translatedCode).toContain('const [first, second] = items;'); + expect(translatedCode).toContain('bru.setEnvVar("userId", id);'); + }); + + it('should handle response in complex conditionals', () => { + const code = ` + if (pm.response.code >= 200 && pm.response.code < 300) { + if (pm.response.headers.get('Content-Type').includes('application/json')) { + const data = pm.response.json(); + + if (data.success === true && data.token) { + pm.environment.set("authToken", data.token); + } else if (data.error) { + console.error("API error:", data.error); + } + } + } else if (pm.response.code === 404) { + console.log("Resource not found"); + } else { + console.error("Request failed with status:", pm.response.code); + } + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('if (res.getStatus() >= 200 && res.getStatus() < 300) {'); + expect(translatedCode).toContain('if (res.getHeaders().get(\'Content-Type\').includes(\'application/json\')) {'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('bru.setEnvVar("authToken", data.token);'); + expect(translatedCode).toContain('} else if (res.getStatus() === 404) {'); + expect(translatedCode).toContain('console.error("Request failed with status:", res.getStatus());'); + }); + + it('should handle response processing with try-catch', () => { + const code = ` + try { + const data = pm.response.json(); + pm.environment.set("userData", JSON.stringify(data.user)); + } catch (error) { + console.error("Failed to parse response:", error); + const text = pm.response.text(); + pm.environment.set("rawResponse", text); + } + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('bru.setEnvVar("userData", JSON.stringify(data.user));'); + expect(translatedCode).toContain('const text = JSON.stringify(res.getBody());'); + expect(translatedCode).toContain('bru.setEnvVar("rawResponse", text);'); + }); + + it('should handle JSON path style access to response data', () => { + const code = ` + const data = pm.response.json(); + const userId = data.user.id; + const userEmail = data.user.contact.email; + const firstItem = data.items[0]; + + pm.environment.set("userId", userId); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('const userId = data.user.id;'); + expect(translatedCode).toContain('const userEmail = data.user.contact.email;'); + expect(translatedCode).toContain('const firstItem = data.items[0];'); + expect(translatedCode).toContain('bru.setEnvVar("userId", userId);'); + }); + + it('should handle template literals with response data', () => { + const code = ` + const data = pm.response.json(); + const welcomeMessage = \`Hello, \${data.user.name}! Your ID is \${data.user.id}.\`; + + pm.environment.set("welcomeMessage", welcomeMessage); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('const welcomeMessage = `Hello, ${data.user.name}! Your ID is ${data.user.id}.`;'); + expect(translatedCode).toContain('bru.setEnvVar("welcomeMessage", welcomeMessage);'); + }); + + it('should handle response processing in arrow functions', () => { + const code = ` + const processItems = () => { + const items = pm.response.json().items; + return items.map(item => item.id); + }; + + const itemIds = processItems(); + pm.environment.set("itemIds", JSON.stringify(itemIds)); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const items = res.getBody().items;'); + expect(translatedCode).toContain('return items.map(item => item.id);'); + expect(translatedCode).toContain('const itemIds = processItems();'); + expect(translatedCode).toContain('bru.setEnvVar("itemIds", JSON.stringify(itemIds));'); + }); + + it('should handle complex inline operations with response data', () => { + const code = ` + const items = pm.response.json().items; + const totalValue = items.reduce((sum, item) => sum + item.price, 0); + const highValueItems = items.filter(item => item.price > 100); + const itemNames = items.map(item => item.name); + + pm.environment.set("totalValue", totalValue); + pm.environment.set("highValueItemCount", highValueItems.length); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const items = res.getBody().items;'); + expect(translatedCode).toContain('const totalValue = items.reduce((sum, item) => sum + item.price, 0);'); + expect(translatedCode).toContain('const highValueItems = items.filter(item => item.price > 100);'); + expect(translatedCode).toContain('const itemNames = items.map(item => item.name);'); + expect(translatedCode).toContain('bru.setEnvVar("totalValue", totalValue);'); + expect(translatedCode).toContain('bru.setEnvVar("highValueItemCount", highValueItems.length);'); + }); + + it('should handle complex test structure with pm.response.to.have.header', () => { + const code = ` + pm.test("Response headers validation", function() { + pm.response.to.have.header("Content-Type", "application/json"); + pm.response.to.have.header("Cache-Control"); + + const responseTime = pm.response.responseTime; + pm.expect(responseTime).to.be.below(1000); + }); + `; + const translatedCode = translateCode(code); + + // Check for test function conversion + expect(translatedCode).toContain('test("Response headers validation", function() {'); + + // Check for header assertions inside the test callback + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase(), "application/json");'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Cache-Control".toLowerCase())'); + + // Check that other test assertions are preserved + expect(translatedCode).toContain('const responseTime = res.getResponseTime();'); + expect(translatedCode).toContain('expect(responseTime).to.be.below(1000);'); + }); + + it('should handle dynamic header names in pm.response.to.have.header', () => { + const code = ` + function checkHeaderPresent(headerName) { + pm.response.to.have.header(headerName); + } + + function validateHeader(headerName, expectedValue) { + pm.response.to.have.header(headerName, expectedValue); + } + + checkHeaderPresent("Authorization"); + validateHeader("Content-Type", "application/json"); + `; + const translatedCode = translateCode(code); + + // Check function transformations + expect(translatedCode).toContain('function checkHeaderPresent(headerName) {'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(headerName.toLowerCase())'); + + expect(translatedCode).toContain('function validateHeader(headerName, expectedValue) {'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(headerName.toLowerCase(), expectedValue);'); + + // Check function calls + expect(translatedCode).toContain('checkHeaderPresent("Authorization");'); + expect(translatedCode).toContain('validateHeader("Content-Type", "application/json");'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/scoped-variables.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/scoped-variables.test.js new file mode 100644 index 000000000..9ed5ed700 --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/scoped-variables.test.js @@ -0,0 +1,51 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Scoped Variables', () => { + it.skip('should handle scoped variables correctly', () => { + const code = ` + const response = pm.response; + const status = response.status; + + function test() { + const response = delta.response; + const status = response.status; + console.log(status); + } + ` + const result = translateCode(code); + console.log(result); + expect(result).toBe(` + const status = res.statusText; + + function test() { + const response = delta.response; + const status = response.status; + console.log(status); + } + `) + }) + + it.skip('should handle scoped variables correctly', () => { + const code = ` + const response = delta.response; + const status = response.status; + + function test() { + const response = pm.response; + const status = response.status; + console.log(status); + } + ` + const result = translateCode(code); + console.log(result); + expect(result).toBe(` + const response = delta.response; + const status = response.status; + + function test() { + const status = res.statusText; + console.log(status); + } + `) + }) +}) \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/testing-framework.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/testing-framework.test.js new file mode 100644 index 000000000..fc3988f1f --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/testing-framework.test.js @@ -0,0 +1,399 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Testing Framework Translation', () => { + // Basic testing framework translations + it('should translate pm.test', () => { + const code = 'pm.test("Status code is 200", function() { pm.response.to.have.status(200); });'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('test("Status code is 200", function() { expect(res.getStatus()).to.equal(200); });'); + }); + + it('should translate pm.expect', () => { + const code = 'pm.expect(jsonData.success).to.be.true;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('expect(jsonData.success).to.be.true;'); + }); + + it('should translate pm.expect.fail', () => { + const code = 'if (!isValid) pm.expect.fail("Data is invalid");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('if (!isValid) expect.fail("Data is invalid");'); + }); + + // Tests with response assertions + it('should translate pm.response.to.have.status in tests', () => { + const code = ` + pm.test("Check environment and call successful", function () { + pm.expect(pm.environment.name).to.equal("ENVIRONMENT_NAME"); + pm.response.to.have.status(200); + });`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Check environment and call successful", function () { + expect(bru.getEnvName()).to.equal("ENVIRONMENT_NAME"); + expect(res.getStatus()).to.equal(200); + });`); + }); + + // Test aliases + it('should handle test aliases', () => { + const code = ` + const { test, expect } = pm; + + test("Status code is 200", function () { + expect(pm.response.code).to.equal(200); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).not.toContain('const { test, expect } = pm'); + expect(translatedCode).toContain('test("Status code is 200", function () {'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);'); + }); + + // Tests inside different code structures + it('should translate pm commands inside tests with nested functions', () => { + const code = ` + pm.test("Auth flow works", function() { + const response = pm.response.json(); + pm.expect(response.authenticated).to.be.true; + pm.environment.set("userId", response.user.id); + pm.collectionVariables.set("sessionId", response.session.id); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Auth flow works", function() {'); + expect(translatedCode).toContain('const response = res.getBody();'); + expect(translatedCode).toContain('expect(response.authenticated).to.be.true;'); + expect(translatedCode).toContain('bru.setEnvVar("userId", response.user.id);'); + expect(translatedCode).toContain('bru.setVar("sessionId", response.session.id);'); + }); + + it('should translate pm.test with arrow functions', () => { + const code = ` + pm.test("Status code is 200", () => { + pm.expect(pm.response.code).to.eql(200); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Status code is 200", () => {'); + expect(translatedCode).toContain('expect(res.getStatus()).to.eql(200);'); + }); + + it('should handle multiple test assertions in one function', () => { + const code = ` + pm.test("The response has all properties", () => { + const responseJson = pm.response.json(); + pm.expect(responseJson.type).to.eql('vip'); + pm.expect(responseJson.name).to.be.a('string'); + pm.expect(responseJson.id).to.have.lengthOf(1); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("The response has all properties", () => {'); + expect(translatedCode).toContain('const responseJson = res.getBody();'); + expect(translatedCode).toContain('expect(responseJson.type).to.eql(\'vip\');'); + expect(translatedCode).toContain('expect(responseJson.name).to.be.a(\'string\');'); + expect(translatedCode).toContain('expect(responseJson.id).to.have.lengthOf(1);'); + }); + + // Test with aliased variables + it('should translate aliases within test functions', () => { + const code = ` + const tempRes = pm.response; + const tempTest = pm.test; + const tempExpect = pm.expect; + + tempTest("Status code is 200", function() { + tempExpect(tempRes.code).to.equal(200); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).not.toContain('const tempRes = pm.response;'); + expect(translatedCode).not.toContain('const tempTest = pm.test;'); + expect(translatedCode).not.toContain('const tempExpect = pm.expect;'); + expect(translatedCode).toContain('test("Status code is 200", function() {'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);'); + }); + + // Additional robust tests for testing framework + it('should handle nested test functions', () => { + const code = ` + pm.test("Main test group", function() { + const responseJson = pm.response.json(); + + pm.test("User data validation", function() { + pm.expect(responseJson.user).to.be.an('object'); + pm.expect(responseJson.user.id).to.be.a('string'); + }); + + pm.test("Settings validation", function() { + pm.expect(responseJson.settings).to.be.an('object'); + pm.expect(responseJson.settings.notifications).to.be.a('boolean'); + }); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Main test group", function() {'); + expect(translatedCode).toContain('const responseJson = res.getBody();'); + expect(translatedCode).toContain('test("User data validation", function() {'); + expect(translatedCode).toContain('expect(responseJson.user).to.be.an(\'object\');'); + expect(translatedCode).toContain('test("Settings validation", function() {'); + expect(translatedCode).toContain('expect(responseJson.settings.notifications).to.be.a(\'boolean\');'); + }); + + it('should handle test with dynamic test names', () => { + const code = ` + const endpoint = pm.variables.get("currentEndpoint"); + + pm.test(\`\${endpoint} returns correct data\`, function() { + const responseJson = pm.response.json(); + pm.expect(responseJson).to.be.an('object'); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const endpoint = bru.getVar("currentEndpoint");'); + expect(translatedCode).toContain('test(`${endpoint} returns correct data`, function() {'); + expect(translatedCode).toContain('const responseJson = res.getBody();'); + expect(translatedCode).toContain('expect(responseJson).to.be.an(\'object\');'); + }); + + it('should handle test with conditional execution', () => { + const code = ` + const responseJson = pm.response.json(); + + if (responseJson.type === 'user') { + pm.test("User validation", function() { + pm.expect(responseJson.name).to.be.a('string'); + pm.expect(responseJson.email).to.be.a('string'); + }); + } else if (responseJson.type === 'admin') { + pm.test("Admin validation", function() { + pm.expect(responseJson.accessLevel).to.be.above(5); + pm.expect(responseJson.permissions).to.be.an('array'); + }); + } + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const responseJson = res.getBody();'); + expect(translatedCode).toContain('if (responseJson.type === \'user\') {'); + expect(translatedCode).toContain('test("User validation", function() {'); + expect(translatedCode).toContain('expect(responseJson.name).to.be.a(\'string\');'); + expect(translatedCode).toContain('} else if (responseJson.type === \'admin\') {'); + expect(translatedCode).toContain('test("Admin validation", function() {'); + expect(translatedCode).toContain('expect(responseJson.accessLevel).to.be.above(5);'); + }); + + it('should handle assertions with logical operators', () => { + const code = ` + pm.test("Response has valid structure", function() { + const data = pm.response.json(); + + pm.expect(data.id && data.name).to.be.ok; + pm.expect(data.active || data.pending).to.be.true; + pm.expect(!data.deleted).to.be.true; + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Response has valid structure", function() {'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('expect(data.id && data.name).to.be.ok;'); + expect(translatedCode).toContain('expect(data.active || data.pending).to.be.true;'); + expect(translatedCode).toContain('expect(!data.deleted).to.be.true;'); + }); + + it('should handle array and object assertions', () => { + const code = ` + pm.test("Array and object validations", function() { + const data = pm.response.json(); + + // Array validations + pm.expect(data.items).to.be.an('array'); + pm.expect(data.items).to.have.lengthOf.at.least(1); + pm.expect(data.items[0]).to.have.property('id'); + + // Object validations + pm.expect(data.user).to.be.an('object'); + pm.expect(data.user).to.have.all.keys('id', 'name', 'email'); + pm.expect(data.user).to.include({active: true}); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Array and object validations", function() {'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('expect(data.items).to.be.an(\'array\');'); + expect(translatedCode).toContain('expect(data.items).to.have.lengthOf.at.least(1);'); + expect(translatedCode).toContain('expect(data.items[0]).to.have.property(\'id\');'); + expect(translatedCode).toContain('expect(data.user).to.be.an(\'object\');'); + expect(translatedCode).toContain('expect(data.user).to.have.all.keys(\'id\', \'name\', \'email\');'); + expect(translatedCode).toContain('expect(data.user).to.include({active: true});'); + }); + + it('should handle chai assertions with deep equality', () => { + const code = ` + pm.test("Deep equality checks", function() { + const data = pm.response.json(); + + pm.expect(data.config).to.deep.equal({ + version: "1.0", + active: true, + features: ["search", "export"] + }); + + pm.expect(data.tags).to.have.members(['api', 'test']); + pm.expect(data.meta).to.deep.include({format: 'json'}); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Deep equality checks", function() {'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('expect(data.config).to.deep.equal({'); + expect(translatedCode).toContain('version: "1.0",'); + expect(translatedCode).toContain('active: true,'); + expect(translatedCode).toContain('features: ["search", "export"]'); + expect(translatedCode).toContain('expect(data.tags).to.have.members([\'api\', \'test\']);'); + expect(translatedCode).toContain('expect(data.meta).to.deep.include({format: \'json\'});'); + }); + + it('should handle chai assertions with string comparisons', () => { + const code = ` + pm.test("String validations", function() { + const data = pm.response.json(); + + pm.expect(data.id).to.be.a('string'); + pm.expect(data.name).to.match(/^[A-Za-z\\s]+$/); + pm.expect(data.description).to.include('API'); + pm.expect(data.url).to.have.string('api/v1'); + pm.expect(data.code).to.have.lengthOf(8); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("String validations", function() {'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('expect(data.id).to.be.a(\'string\');'); + expect(translatedCode).toContain('expect(data.name).to.match(/^[A-Za-z\\s]+$/);'); + expect(translatedCode).toContain('expect(data.description).to.include(\'API\');'); + expect(translatedCode).toContain('expect(data.url).to.have.string(\'api/v1\');'); + expect(translatedCode).toContain('expect(data.code).to.have.lengthOf(8);'); + }); + + it('should handle assertions with numeric comparisons', () => { + const code = ` + pm.test("Numeric validations", function() { + const data = pm.response.json(); + + pm.expect(data.count).to.be.a('number'); + pm.expect(data.count).to.be.above(0); + pm.expect(data.price).to.be.within(10, 100); + pm.expect(data.discount).to.be.at.most(25); + pm.expect(data.quantity * data.price).to.equal(data.total); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Numeric validations", function() {'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('expect(data.count).to.be.a(\'number\');'); + expect(translatedCode).toContain('expect(data.count).to.be.above(0);'); + expect(translatedCode).toContain('expect(data.price).to.be.within(10, 100);'); + expect(translatedCode).toContain('expect(data.discount).to.be.at.most(25);'); + expect(translatedCode).toContain('expect(data.quantity * data.price).to.equal(data.total);'); + }); + + it('should handle pm.expect.fail with conditions', () => { + const code = ` + pm.test("Validate critical fields", function() { + const data = pm.response.json(); + + if (!data.id) { + pm.expect.fail("Missing ID field"); + } + + if (data.status !== 'active' && data.status !== 'pending') { + pm.expect.fail("Invalid status: " + data.status); + } + + // Continue with normal assertions + pm.expect(data.name).to.be.a('string'); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Validate critical fields", function() {'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('if (!data.id) {'); + expect(translatedCode).toContain('expect.fail("Missing ID field");'); + expect(translatedCode).toContain('if (data.status !== \'active\' && data.status !== \'pending\') {'); + expect(translatedCode).toContain('expect.fail("Invalid status: " + data.status);'); + expect(translatedCode).toContain('expect(data.name).to.be.a(\'string\');'); + }); + + it('should handle complex test compositions', () => { + const code = ` + // Helper function + function validateUserObject(user) { + pm.expect(user).to.be.an('object'); + pm.expect(user.id).to.be.a('string'); + pm.expect(user.name).to.be.a('string'); + return user.id && user.name; + } + + pm.test("Response validation", function() { + const response = pm.response.json(); + const validUsers = []; + + // Test status code + pm.response.to.have.status(200); + + // Test main user + if (response.user) { + const isValid = validateUserObject(response.user); + if (isValid) { + validUsers.push(response.user); + } + } + + // Test related users + if (response.relatedUsers && Array.isArray(response.relatedUsers)) { + pm.test("Related users validation", function() { + response.relatedUsers.forEach((user, index) => { + pm.test(\`User at index \${index}\`, function() { + const isValid = validateUserObject(user); + if (isValid) { + validUsers.push(user); + } + }); + }); + }); + } + + // Set the valid users for later use + if (validUsers.length > 0) { + pm.environment.set("validUsers", JSON.stringify(validUsers)); + } + }); + `; + const translatedCode = translateCode(code); + + // Test key transformations + expect(translatedCode).toContain('function validateUserObject(user) {'); + expect(translatedCode).toContain('expect(user).to.be.an(\'object\');'); + expect(translatedCode).toContain('test("Response validation", function() {'); + expect(translatedCode).toContain('const response = res.getBody();'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);'); + expect(translatedCode).toContain('test("Related users validation", function() {'); + expect(translatedCode).toContain('test(`User at index ${index}`, function() {'); + expect(translatedCode).toContain('bru.setEnvVar("validUsers", JSON.stringify(validUsers));'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variable-chaining.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variable-chaining.test.js new file mode 100644 index 000000000..3c700000e --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variable-chaining.test.js @@ -0,0 +1,91 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Variable Chaining Resolution', () => { + test('should resolve a simple variable chain (variable pointing to another variable)', () => { + const code = ` + const original = pm.response; + const alias = original; + const data = alias.json(); + `; + + const translatedCode = translateCode(code); + + // Check that alias.json() was properly resolved to res.getBody() + expect(translatedCode).toContain('const data = res.getBody();'); + // The original variable declarations should be removed + expect(translatedCode).not.toContain('const original ='); + expect(translatedCode).not.toContain('const alias ='); + }); + + test('should handle mixed variable references correctly', () => { + const code = ` + const respVar = pm.response; + const envVar = pm.environment; + const respAlias = respVar; + + // These should be replaced + const statusCode = respAlias.code; + const envValue = envVar.get("key"); + + // This should not be replaced + const unrelatedVar = "some value"; + `; + + const translatedCode = translateCode(code); + + // Check correct replacements + expect(translatedCode).not.toContain('const respVar'); + expect(translatedCode).not.toContain('const envVar'); + expect(translatedCode).toContain('const statusCode = res.getStatus();'); + expect(translatedCode).toContain('const envValue = bru.getEnvVar("key");'); + + // Check that unrelated variables are preserved + expect(translatedCode).toContain('const unrelatedVar = "some value";'); + }); + + /** + * This test verifies that when multiple variables are declared in a single statement, + * only the ones referencing Postman objects are removed and the others are preserved. + * + * For example, in a statement like: + * const response = pm.response, counter = 5, helper = "test"; + * + * Only 'response' should be removed, resulting in: + * const counter = 5, helper = "test"; + */ + test('should handle multiple variables in one declaration statement', () => { + const code = ` + // Multiple variables in one declaration, with a mix of Postman objects and regular variables + const response = pm.response, counter = 5, helper = "test"; + + // Using both the Postman reference (should be replaced) and regular values (should be preserved) + const statusCode = response.code; + console.log("Counter value:", counter); + console.log("Helper string:", helper); + + // Another example with different Postman object + let env = pm.environment, timeout = 1000, isValid = true; + const baseUrl = env.get("baseUrl"); + `; + + const translatedCode = translateCode(code); + + // Postman references should be replaced + expect(translatedCode).not.toContain('response = pm.response'); + expect(translatedCode).not.toContain('env = pm.environment'); + + // Regular variables should be preserved + expect(translatedCode).toContain('const counter = 5'); + expect(translatedCode).toContain('helper = "test"'); + expect(translatedCode).toContain('timeout = 1000'); + expect(translatedCode).toContain('isValid = true'); + + // References to Postman objects should be properly translated + expect(translatedCode).toContain('const statusCode = res.getStatus();'); + expect(translatedCode).toContain('const baseUrl = bru.getEnvVar("baseUrl");'); + + // Console logs with regular variables should be preserved + expect(translatedCode).toContain('console.log("Counter value:", counter);'); + expect(translatedCode).toContain('console.log("Helper string:", helper);'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variables.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variables.test.js new file mode 100644 index 000000000..b704e4a7e --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variables.test.js @@ -0,0 +1,128 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Variables Translation', () => { + // Regular variables tests + it('should translate pm.variables.get', () => { + const code = 'pm.variables.get("test");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.getVar("test");'); + }); + + it('should translate pm.variables.set', () => { + const code = 'pm.variables.set("test", "value");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setVar("test", "value");'); + }); + + it('should translate pm.variables.has', () => { + const code = 'pm.variables.has("userId");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.hasVar("userId");'); + }); + + // Collection variables tests + it('should translate pm.collectionVariables.get', () => { + const code = 'pm.collectionVariables.get("apiUrl");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.getVar("apiUrl");'); + }); + + it('should translate pm.collectionVariables.set', () => { + const code = 'pm.collectionVariables.set("token", jsonData.token);'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setVar("token", jsonData.token);'); + }); + + it('should translate pm.collectionVariables.has', () => { + const code = 'pm.collectionVariables.has("authToken");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.hasVar("authToken");'); + }); + + it('should translate pm.collectionVariables.unset', () => { + const code = 'pm.collectionVariables.unset("tempVar");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.deleteVar("tempVar");'); + }); + + // Alias tests for variables + it('should handle variables aliases', () => { + const code = ` + const vars = pm.variables; + const has = vars.has("test"); + const set = vars.set("test", "value"); + const get = vars.get("test"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const has = bru.hasVar("test"); + const set = bru.setVar("test", "value"); + const get = bru.getVar("test"); + `); + }); + + // Alias tests for collection variables + it('should handle collection variables aliases', () => { + const code = ` + const collVars = pm.collectionVariables; + const has = collVars.has("test"); + const set = collVars.set("test", "value"); + const get = collVars.get("test"); + const unset = collVars.unset("test"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const has = bru.hasVar("test"); + const set = bru.setVar("test", "value"); + const get = bru.getVar("test"); + const unset = bru.deleteVar("test"); + `); + }); + + // Combined tests + it('should handle conditional expressions with variable calls', () => { + const code = 'const userStatus = pm.variables.has("userId") ? "logged-in" : "guest";'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const userStatus = bru.hasVar("userId") ? "logged-in" : "guest";'); + }); + + it('should handle all variable methods together', () => { + const code = ` + // All variable methods + const hasUserId = pm.variables.has("userId"); + const userId = pm.variables.get("userId"); + pm.variables.set("requestTime", new Date().toISOString()); + + console.log(\`Has userId: \${hasUserId}, User ID: \${userId}\`); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const hasUserId = bru.hasVar("userId");'); + expect(translatedCode).toContain('const userId = bru.getVar("userId");'); + expect(translatedCode).toContain('bru.setVar("requestTime", new Date().toISOString());'); + }); + + it('should handle all collection variable methods together', () => { + const code = ` + // All collection variable methods + const hasApiUrl = pm.collectionVariables.has("apiUrl"); + const apiUrl = pm.collectionVariables.get("apiUrl"); + pm.collectionVariables.set("requestTime", new Date().toISOString()); + pm.collectionVariables.unset("tempVar"); + + console.log(\`Has API URL: \${hasApiUrl}, API URL: \${apiUrl}\`); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const hasApiUrl = bru.hasVar("apiUrl");'); + expect(translatedCode).toContain('const apiUrl = bru.getVar("apiUrl");'); + expect(translatedCode).toContain('bru.setVar("requestTime", new Date().toISOString());'); + expect(translatedCode).toContain('bru.deleteVar("tempVar");'); + }); + + it('should handle more complex nested expressions with variables', () => { + const code = 'pm.collectionVariables.set("fullPath", pm.environment.get("baseUrl") + pm.variables.get("endpoint"));'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setVar("fullPath", bru.getEnvVar("baseUrl") + bru.getVar("endpoint"));'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/utils/getMemberExpressionString.test.js b/packages/bruno-converters/tests/utils/getMemberExpressionString.test.js new file mode 100644 index 000000000..f5dd69f09 --- /dev/null +++ b/packages/bruno-converters/tests/utils/getMemberExpressionString.test.js @@ -0,0 +1,53 @@ +import { describe, it, expect } from '@jest/globals'; +import { getMemberExpressionString } from '../../src/utils/jscode-shift-translator'; +const j = require('jscodeshift'); + +describe('getMemberExpressionString', () => { + it('should correctly convert simple member expressions to strings', () => { + // Create a simple member expression: pm.environment.get + const memberExpr = j.memberExpression( + j.memberExpression( + j.identifier('pm'), + j.identifier('environment') + ), + j.identifier('get') + ); + + const result = getMemberExpressionString(memberExpr); + expect(result).toBe('pm.environment.get'); + }); + + it('should handle computed properties with string literals', () => { + // Create a computed member expression: pm["environment"]["get"] + const memberExpr = j.memberExpression( + j.memberExpression( + j.identifier('pm'), + j.literal('environment'), + true // computed + ), + j.literal('get'), + true // computed + ); + + const result = getMemberExpressionString(memberExpr); + expect(result).toBe('pm.environment.get'); + }); + + it('should mark non-string computed properties as [computed]', () => { + // Create a computed member expression with variable: obj[varName] + const memberExpr = j.memberExpression( + j.identifier('obj'), + j.identifier('varName'), + true // computed + ); + + const result = getMemberExpressionString(memberExpr); + expect(result).toBe('obj.[computed]'); + }); + + it('should handle basic identifiers', () => { + const identifier = j.identifier('pm'); + const result = getMemberExpressionString(identifier); + expect(result).toBe('pm'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index dbc921211..26f327b73 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -28,6 +28,7 @@ "@aws-sdk/credential-providers": "3.750.0", "@faker-js/faker": "^9.5.1", "@usebruno/common": "0.1.0", + "@usebruno/converters": "^0.1.0", "@usebruno/js": "0.12.0", "@usebruno/lang": "0.12.0", "@usebruno/node-machine-id": "^2.0.0", diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index 3ee646e81..a2f91d953 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -220,7 +220,6 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread } } - // Is this a folder.bru file? if (path.basename(pathname) === 'folder.bru') { const file = { meta: { @@ -327,16 +326,26 @@ const addDirectory = async (win, pathname, collectionUid, collectionPath) => { } let name = path.basename(pathname); + let seq = 1; + const folderBruFilePath = path.join(pathname, `folder.bru`); + + if (fs.existsSync(folderBruFilePath)) { + let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8'); + let folderBruData = await collectionBruToJson(folderBruFileContent); + name = folderBruData?.meta?.name || name; + seq = folderBruData?.meta?.seq || seq; + } const directory = { meta: { collectionUid, pathname, - name + name, + seq, + uid: getRequestUid(pathname) } }; - win.webContents.send('main:collection-tree-updated', 'addDir', directory); }; diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js index d41c980d7..946d95519 100644 --- a/packages/bruno-electron/src/bru/index.js +++ b/packages/bruno-electron/src/bru/index.js @@ -29,9 +29,11 @@ const collectionBruToJson = async (data, parsed = false) => { // add meta if it exists // this is only for folder bru file // in the future, all of this will be replaced by standard bru lang - if (json.meta) { + const sequence = _.get(json, 'meta.seq'); + if (json?.meta) { transformedJson.meta = { - name: json.meta.name + name: json.meta.name, + seq: !isNaN(sequence) ? Number(sequence) : 1 }; } @@ -61,9 +63,11 @@ const jsonToCollectionBru = async (json, isFolder) => { // add meta if it exists // this is only for folder bru file // in the future, all of this will be replaced by standard bru lang + const sequence = _.get(json, 'meta.seq'); if (json?.meta) { collectionBruJson.meta = { - name: json.meta.name + name: json.meta.name, + seq: !isNaN(sequence) ? Number(sequence) : 1 }; } diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 0aaf87d55..b0924bb6b 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -1,10 +1,13 @@ const _ = require('lodash'); const fs = require('fs'); +const fsPromises = require('fs/promises'); const fsExtra = require('fs-extra'); const os = require('os'); const path = require('path'); const { ipcMain, shell, dialog, app } = require('electron'); const { envJsonToBru, bruToJson, jsonToBru, jsonToBruViaWorker, collectionBruToJson, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru'); +const brunoConverters = require('@usebruno/converters'); +const { postmanToBruno } = brunoConverters; const { writeFile, @@ -22,7 +25,10 @@ const { hasSubDirectories, getCollectionStats, sizeInMB, - safeWriteFileSync + safeWriteFileSync, + copyPath, + removePath, + getPaths } = require('../utils/filesystem'); const { openCollectionDialog } = require('../app/collections'); const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common'); @@ -32,11 +38,10 @@ const EnvironmentSecretsStore = require('../store/env-secrets'); const CollectionSecurityStore = require('../store/collection-security'); const UiStateSnapshotStore = require('../store/ui-state-snapshot'); const interpolateVars = require('./network/interpolate-vars'); -const { getEnvVars, getTreePathFromCollectionToItem, mergeVars } = require('../utils/collection'); +const { getEnvVars, getTreePathFromCollectionToItem, mergeVars, parseBruFileMeta, hydrateRequestWithUuid, transformRequestToSaveToFilesystem } = require('../utils/collection'); const { getProcessEnvVars } = require('../store/process-env'); const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, refreshOauth2Token } = require('../utils/oauth2'); const { getCertsAndProxyConfig } = require('./network'); -const { parseBruFileMeta, hydrateRequestWithUuid } = require('../utils/collection'); const environmentSecretsStore = new EnvironmentSecretsStore(); const collectionSecurityStore = new CollectionSecurityStore(); @@ -192,12 +197,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection ipcMain.handle('renderer:save-folder-root', async (event, folder) => { try { - const { name: folderName, root: folderRoot, pathname: folderPathname } = folder; + const { name: folderName, root: folderRoot = {}, pathname: folderPathname } = folder; const folderBruFilePath = path.join(folderPathname, 'folder.bru'); - folderRoot.meta = { - name: folderName - }; + if (!folderRoot.meta) { + folderRoot.meta = { + name: folderName, + seq: 1 + }; + } const content = await jsonToCollectionBru( folderRoot, @@ -376,14 +384,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection if (fs.existsSync(folderBruFilePath)) { const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8'); folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent); + folderBruFileJsonContent.meta.name = newName; } else { - folderBruFileJsonContent = {}; + folderBruFileJsonContent = { + meta: { + name: newName, + seq: 1 + } + }; } - - folderBruFileJsonContent.meta = { - name: newName, - }; - + const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true); await writeFile(folderBruFilePath, folderBruFileContent); @@ -425,14 +435,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection if (fs.existsSync(folderBruFilePath)) { const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8'); folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent); + folderBruFileJsonContent.meta.name = newName; } else { - folderBruFileJsonContent = {}; + folderBruFileJsonContent = { + meta: { + name: newName, + seq: 1 + } + }; } - folderBruFileJsonContent.meta = { - name: newName, - }; - const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true); await writeFile(folderBruFilePath, folderBruFileContent); @@ -512,6 +524,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection let data = { meta: { name: folderName, + seq: 1 } }; const content = await jsonToCollectionBru(data, true); // isFolder flag @@ -598,6 +611,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection if (item?.root?.meta?.name) { const folderBruFilePath = path.join(folderPath, 'folder.bru'); + item.root.meta.seq = item.seq; const folderContent = await jsonToCollectionBru( item.root, true // isFolder @@ -731,17 +745,42 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection ipcMain.handle('renderer:resequence-items', async (event, itemsToResequence) => { try { - for await (let item of itemsToResequence) { - const bru = fs.readFileSync(item.pathname, 'utf8'); - const jsonData = await bruToJsonViaWorker(bru); - - if (jsonData.seq !== item.seq) { - jsonData.seq = item.seq; - const content = await jsonToBruViaWorker(jsonData); - await writeFile(item.pathname, content); + for (let item of itemsToResequence) { + if (item?.type === 'folder') { + const folderRootPath = path.join(item.pathname, 'folder.bru'); + let folderBruJsonData = { + meta: { + name: path.basename(item?.pathname), + seq: item?.seq || 1 + } + }; + if (fs.existsSync(folderRootPath)) { + const bru = fs.readFileSync(folderRootPath, 'utf8'); + folderBruJsonData = await collectionBruToJson(bru); + if (!folderBruJsonData?.meta) { + folderBruJsonData.meta = { + name: path.basename(item?.pathname), + seq: item?.seq || 1 + }; + } + if (folderBruJsonData?.meta?.seq === item.seq) { + continue; + } + folderBruJsonData.meta.seq = item.seq; + } + const content = await jsonToCollectionBru(folderBruJsonData); + await writeFile(folderRootPath, content); + } else { + if (fs.existsSync(item.pathname)) { + const itemToSave = transformRequestToSaveToFilesystem(item); + const content = await jsonToBruViaWorker(itemToSave); + await writeFile(item.pathname, content); + } } } + return true; } catch (error) { + console.error('Error in resequence-items:', error); return Promise.reject(error); } }); @@ -760,6 +799,24 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }); + ipcMain.handle('renderer:move-item', async (event, { targetDirname, sourcePathname }) => { + try { + if (fs.existsSync(targetDirname)) { + const sourceDirname = path.dirname(sourcePathname); + const pathnamesBefore = await getPaths(sourcePathname); + const pathnamesAfter = pathnamesBefore?.map(p => p?.replace(sourceDirname, targetDirname)); + await copyPath(sourcePathname, targetDirname); + await removePath(sourcePathname); + // move the request uids of the previous file/folders to the new file/folder items + pathnamesAfter?.forEach((_, index) => { + moveRequestUid(pathnamesBefore[index], pathnamesAfter[index]); + }); + } + } catch (error) { + return Promise.reject(error); + } + }); + ipcMain.handle('renderer:move-folder-item', async (event, folderPath, destinationPath) => { try { const folderName = path.basename(folderPath); @@ -1096,6 +1153,19 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection throw error; } }); + + // Implement the Postman to Bruno conversion handler + ipcMain.handle('renderer:convert-postman-to-bruno', async (event, postmanCollection) => { + try { + // Convert Postman collection to Bruno format + const brunoCollection = await postmanToBruno(postmanCollection, { useWorkers: true}); + + return brunoCollection; + } catch (error) { + console.error('Error converting Postman to Bruno:', error); + return Promise.reject(error); + } + }); }; const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => { diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 8fddd2d98..28a49e80f 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -1255,6 +1255,7 @@ const registerNetworkIpc = (mainWindow) => { folderUid }); } catch (error) { + console.log("error", error); deleteCancelToken(cancelTokenUid); mainWindow.webContents.send('main:run-folder-event', { type: 'testrun-ended', diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index d6fed9da6..94fa30ec8 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -1,4 +1,4 @@ -const { get, each, find, compact, filter } = require('lodash'); +const { get, each, find, compact, isString, filter } = require('lodash'); const fs = require('fs'); const { getRequestUid } = require('../cache/requestUids'); const { uuid } = require('./common'); @@ -205,6 +205,14 @@ const findParentItemInCollection = (collection, itemUid) => { }); }; +const findParentItemInCollectionByPathname = (collection, pathname) => { + let flattenedItems = flattenItems(collection.items); + + return find(flattenedItems, (item) => { + return item.items && find(item.items, (i) => i.pathname === pathname); + }); +}; + const getTreePathFromCollectionToItem = (collection, _item) => { let path = []; let item = findItemInCollection(collection, _item.uid); @@ -272,12 +280,73 @@ const findItemInCollectionByPathname = (collection, pathname) => { return findItemByPathname(flattenedItems, pathname); }; +const replaceTabsWithSpaces = (str, numSpaces = 2) => { + if (!str || !str.length || !isString(str)) { + return ''; + } + + return str.replaceAll('\t', ' '.repeat(numSpaces)); +}; + +const transformRequestToSaveToFilesystem = (item) => { + const _item = item.draft ? item.draft : item; + const itemToSave = { + uid: _item.uid, + type: _item.type, + name: _item.name, + seq: _item.seq, + request: { + method: _item.request.method, + url: _item.request.url, + params: [], + headers: [], + auth: _item.request.auth, + body: _item.request.body, + script: _item.request.script, + vars: _item.request.vars, + assertions: _item.request.assertions, + tests: _item.request.tests, + docs: _item.request.docs + } + }; + + each(_item.request.params, (param) => { + itemToSave.request.params.push({ + uid: param.uid, + name: param.name, + value: param.value, + description: param.description, + type: param.type, + enabled: param.enabled + }); + }); + + each(_item.request.headers, (header) => { + itemToSave.request.headers.push({ + uid: header.uid, + name: header.name, + value: header.value, + description: header.description, + enabled: header.enabled + }); + }); + + if (itemToSave.request.body.mode === 'json') { + itemToSave.request.body = { + ...itemToSave.request.body, + json: replaceTabsWithSpaces(itemToSave.request.body.json) + }; + } + + return itemToSave; +} + const sortCollection = (collection) => { const items = collection.items || []; let folderItems = filter(items, (item) => item.type === 'folder'); let requestItems = filter(items, (item) => item.type !== 'folder'); - folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name)); + folderItems = folderItems.sort((a, b) => a.seq - b.seq); requestItems = requestItems.sort((a, b) => a.seq - b.seq); collection.items = folderItems.concat(requestItems); @@ -292,7 +361,7 @@ const sortFolder = (folder = {}) => { let folderItems = filter(items, (item) => item.type === 'folder'); let requestItems = filter(items, (item) => item.type !== 'folder'); - folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name)); + folderItems = folderItems.sort((a, b) => a.seq - b.seq); requestItems = requestItems.sort((a, b) => a.seq - b.seq); folder.items = folderItems.concat(requestItems); @@ -410,11 +479,13 @@ module.exports = { findItemByPathname, findItemInCollectionByPathname, findParentItemInCollection, + findParentItemInCollectionByPathname, parseBruFileMeta, + hydrateRequestWithUuid, + transformRequestToSaveToFilesystem, sortCollection, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, - getFormattedCollectionOauth2Credentials, - hydrateRequestWithUuid + getFormattedCollectionOauth2Credentials }; \ No newline at end of file diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index a95c116eb..28b479013 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -164,9 +164,9 @@ const searchForBruFiles = (dir) => { const sanitizeName = (name) => { const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g; name = name - .replace(invalidCharacters, '-') // replace invalid characters with hyphens - .replace(/^[.\s]+/, '') // remove leading dots and and spaces - .replace(/[.\s]+$/, ''); // remove trailing dots and spaces (keep trailing hyphens) + .replace(invalidCharacters, '-') // replace invalid characters with hyphens + .replace(/^[\s\-]+/, '') // remove leading spaces and hyphens + .replace(/[.\s]+$/, ''); // remove trailing dots and spaces return name; }; @@ -175,10 +175,11 @@ const isWindowsOS = () => { } const validateName = (name) => { + const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g; // keeping this for informational purpose const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i; - const firstCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot, space, or hyphen at start - const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no invalid characters - const lastCharacter = /[^.\s]$/; // no dot or space at end, hyphen allowed + const firstCharacter = /^[^\s\-<>:"/\\|?*\x00-\x1F]/; // no space, hyphen and `invalidCharacters` + const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no `invalidCharacters` + const lastCharacter = /[^.\s<>:"/\\|?*\x00-\x1F]$/; // no dot, space and `invalidCharacters` if (name.length > 255) return false; // max name length if (reservedDeviceNames.test(name)) return false; // windows reserved names @@ -282,6 +283,67 @@ function safeWriteFileSync(filePath, data) { fs.writeFileSync(safePath, data); } +// Recursively copies a source to a destination . +const copyPath = async (source, destination) => { + let targetPath = `${destination}/${path.basename(source)}`; + + const targetPathExists = await fsPromises.access(targetPath).then(() => true).catch(() => false); + if (targetPathExists) { + throw new Error(`Cannot copy, ${path.basename(source)} already exists in ${path.basename(destination)}`); + } + + const copy = async (source, destination) => { + const stat = await fsPromises.lstat(source); + if (stat.isDirectory()) { + await fsPromises.mkdir(destination, { recursive: true }); + const entries = await fsPromises.readdir(source); + for (const entry of entries) { + const srcPath = path.join(source, entry); + const destPath = path.join(destination, entry); + await copy(srcPath, destPath); + } + } else { + await fsPromises.copyFile(source, destination); + } + } + + await copy(source, targetPath); +} + +// Recursively removes a source . +const removePath = async (source) => { + const stat = await fsPromises.lstat(source); + if (stat.isDirectory()) { + const entries = await fsPromises.readdir(source); + for (const entry of entries) { + const entryPath = path.join(source, entry); + await removePath(entryPath); + } + await fsPromises.rmdir(source); + } else { + await fsPromises.unlink(source); + } +} + +// Recursively gets paths. +const getPaths = async (source) => { + let paths = []; + const _getPaths = async (source) => { + const stat = await fsPromises.lstat(source); + paths.push(source); + if (stat.isDirectory()) { + const entries = await fsPromises.readdir(source); + for (const entry of entries) { + const entryPath = path.join(source, entry); + await _getPaths(entryPath); + } + } + } + await _getPaths(source); + return paths; +} + + module.exports = { isValidPathname, exists, @@ -308,5 +370,8 @@ module.exports = { getCollectionStats, sizeInMB, safeWriteFile, - safeWriteFileSync + safeWriteFileSync, + copyPath, + removePath, + getPaths }; diff --git a/packages/bruno-electron/src/utils/filesystem.test.js b/packages/bruno-electron/src/utils/filesystem.test.js index a6e2db53a..a0f0f018d 100644 --- a/packages/bruno-electron/src/utils/filesystem.test.js +++ b/packages/bruno-electron/src/utils/filesystem.test.js @@ -2,9 +2,13 @@ const { sanitizeName, isWSLPath, normalizeWSLPath, normalizeAndResolvePath } = r describe('sanitizeName', () => { it('should replace invalid characters with hyphens', () => { - const input = '<>:"/\|?*\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F'; - const expectedOutput = '----------------------------------------'; - expect(sanitizeName(input)).toEqual(expectedOutput); + expect(sanitizeName( + 'valid<>:"/\|?*\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F' + )).toEqual('valid----------------------------------------'); + + expect(sanitizeName( + '<>:"/\|?*\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1Fvalid<>:"/\|?*\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F' + )).toEqual('valid----------------------------------------'); }); it('should not modify valid directory names', () => { diff --git a/packages/bruno-electron/src/utils/tests/filesystem/index.spec.js b/packages/bruno-electron/src/utils/tests/filesystem/index.spec.js new file mode 100644 index 000000000..60add1b57 --- /dev/null +++ b/packages/bruno-electron/src/utils/tests/filesystem/index.spec.js @@ -0,0 +1,116 @@ +const path = require('path'); +const fs = require('fs/promises'); +const os = require('os'); +const { copyPath, removePath } = require('../../filesystem'); +const { initialCollectionStructure, finalCollectionStructure } = require('../fixtures/filesystem/copypath-removepath'); + +describe('File System Operations', () => { + let tempDir; + + beforeAll(async () => { + // Create a temporary directory for each test + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bruno-test-')); + await createFilesAndFolders(tempDir, initialCollectionStructure); + const result = await verifyFilesAndFolders(tempDir, initialCollectionStructure); + expect(result).toBe(true); + }); + + afterAll(async () => { + // clean up after each test + await fs.rm(tempDir, { recursive: true, force: true }); + // confirm the temp directory is deleted + expect(await fs.access(tempDir).then(() => true).catch(() => false)).toBe(false); + }); + + describe('copyPath and removePath', () => { + it('should move files and folder items multiple times', async () => { + + { + const sourcePath = path.join(tempDir, 'folder_1', 'file_2.bru'); + const destDir = path.join(tempDir, 'folder_1', 'folder_1_1'); + await copyPath(sourcePath, destDir); + await removePath(sourcePath); + } + + { + const sourcePath = path.join(tempDir, 'folder_2'); + const destDir = path.join(tempDir, 'folder_1', 'folder_1_1'); + await copyPath(sourcePath, destDir); + await removePath(sourcePath); + } + + { + const sourcePath = path.join(tempDir, 'folder_1', 'folder_1_1', 'folder_2', 'file_2_2.bru'); + const destDir = path.join(tempDir, 'folder_1'); + await copyPath(sourcePath, destDir); + await removePath(sourcePath); + } + + { + const sourcePath = path.join(tempDir, 'folder_1', 'folder_1_1', 'folder_2', 'folder_2_1'); + const destDir = path.join(tempDir); + await copyPath(sourcePath, destDir); + await removePath(sourcePath); + } + + const result = await verifyFilesAndFolders(tempDir, finalCollectionStructure); + expect(result).toBe(true); + }); + + + it('should throw an error move file/folder if the destination has the same filename', async () => { + { + const sourcePath = path.join(tempDir, 'folder_1', 'file_dup.bru'); + const destDir = path.join(tempDir, 'folder_1'); + await expect(copyPath(sourcePath, destDir)).rejects.toThrow(); + } + }); + + }); +}); + + +// create folders and files recursively based on the defined json structure +const createFilesAndFolders = async (dir, filesAndFolders) => { + for (const item of filesAndFolders) { + const itemPath = path.join(dir, item.name); + if (item.type === 'folder') { + await fs.mkdir(itemPath, { recursive: true }); + await createFilesAndFolders(itemPath, item.files); + } else { + await fs.writeFile(itemPath, item.content); + } + } +} + +// if a file/folder doesnt exist, return false +// should only contain files and folders that are defined in the json structure +const verifyFilesAndFolders = async (dir, filesAndFolders) => { + const verify = async (dir, filesAndFolders) => { + const files = await fs.readdir(dir); + if (files.length !== filesAndFolders.length) { + return false; + } + for (const file of files) { + const itemPath = path.join(dir, file); + const item = filesAndFolders.find(f => f.name === file); + if (!item) { + return false; + } + if (item.type === 'folder') { + return await verify(itemPath, item.files); + } else { + return await fs.readFile(itemPath, 'utf8').then(content => content === item.content); + } + } + return true; + } + + try { + const verified = await verify(dir, filesAndFolders); + return verified; + } catch (error) { + console.error(error); + return false; + } +} \ No newline at end of file diff --git a/packages/bruno-electron/src/utils/tests/fixtures/filesystem/copypath-removepath.js b/packages/bruno-electron/src/utils/tests/fixtures/filesystem/copypath-removepath.js new file mode 100644 index 000000000..ea08f8d25 --- /dev/null +++ b/packages/bruno-electron/src/utils/tests/fixtures/filesystem/copypath-removepath.js @@ -0,0 +1,155 @@ +const initialCollectionStructure = [ + { + "name": "folder_1", + "type": "folder", + "files": [ + { + "name": "file_1.bru", + "type": "file", + "content": "file_1_content" + }, + { + "name": "file_2.bru", + "type": "file", + "content": "file_2_content" + }, + { + "name": "folder_1_1", + "type": "folder", + "files": [ + { + "name": "file_1_1.bru", + "type": "file", + "content": "file_1_1_content" + }, + { + "name": "file_1_2.bru", + "type": "file", + "content": "file_1_2_content" + } + ] + }, + { + "name": "file_1_3.bru", + "type": "file", + "content": "file_1_3_content" + }, + { + "name": "file_dup.bru", + "type": "file", + "content": "file_dup_content" + } + ], + }, + { + "name": "folder_2", + "type": "folder", + "files": [ + { + "name": "file_2_1.bru", + "type": "file", + "content": "file_2_1_content" + }, + { + "name": "file_2_2.bru", + "type": "file", + "content": "file_2_2_content" + }, + { + "name": "folder_2_1", + "type": "folder", + "files": [ + { + "name": "file_2_1_1.bru", + "type": "file", + "content": "file_2_1_1_content" + } + ] + } + ] + }, + { + "name": "file_dup.bru", + "type": "file", + "content": "file_dup_content" + } +]; + +const finalCollectionStructure = [ + { + "name": "folder_1", + "type": "folder", + "files": [ + { + "name": "file_1.bru", + "type": "file", + "content": "file_1_content" + }, + { + "name": "folder_1_1", + "type": "folder", + "files": [ + { + "name": "file_1_1.bru", + "type": "file", + "content": "file_1_1_content" + }, + { + "name": "file_1_2.bru", + "type": "file", + "content": "file_1_2_content" + }, + { + "name": "file_2.bru", + "type": "file", + "content": "file_2_content" + }, + { + "name": "folder_2", + "type": "folder", + "files": [ + { + "name": "file_2_1.bru", + "type": "file", + "content": "file_2_1_content" + } + ] + } + ] + }, + { + "name": "file_1_3.bru", + "type": "file", + "content": "file_1_3_content" + }, + { + "name": "file_2_2.bru", + "type": "file", + "content": "file_2_2_content" + }, + { + "name": "file_dup.bru", + "type": "file", + "content": "file_dup_content" + } + ], + }, + { + "name": "folder_2_1", + "type": "folder", + "files": [ + { + "name": "file_2_1_1.bru", + "type": "file", + "content": "file_2_1_1_content" + } + ] + }, + { + "name": "file_dup.bru", + "type": "file", + "content": "file_dup_content" + } +]; + +module.exports = { initialCollectionStructure, finalCollectionStructure }; \ No newline at end of file diff --git a/packages/bruno-js/package.json b/packages/bruno-js/package.json index d9b4e16e9..c040244a3 100644 --- a/packages/bruno-js/package.json +++ b/packages/bruno-js/package.json @@ -35,6 +35,7 @@ "node-vault": "^0.10.2", "path": "^0.12.7", "quickjs-emscripten": "^0.29.2", + "tv4": "^1.3.0", "uuid": "^9.0.0", "xml2js": "^0.6.2" }, diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index 2a8d02a87..bbb0476f5 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -30,6 +30,7 @@ const CryptoJS = require('crypto-js'); const NodeVault = require('node-vault'); const xml2js = require('xml2js'); const cheerio = require('cheerio'); +const tv4 = require('tv4'); const { executeQuickJsVmAsync } = require('../sandbox/quickjs'); class ScriptRuntime { @@ -151,6 +152,7 @@ class ScriptRuntime { 'crypto-js': CryptoJS, 'xml2js': xml2js, cheerio, + tv4, ...whitelistedModules, fs: allowScriptFilesystemAccess ? fs : undefined, 'node-vault': NodeVault @@ -285,6 +287,7 @@ class ScriptRuntime { 'crypto-js': CryptoJS, 'xml2js': xml2js, cheerio, + tv4, ...whitelistedModules, fs: allowScriptFilesystemAccess ? fs : undefined, 'node-vault': NodeVault diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js index e2d1f4865..f1a5ef572 100644 --- a/packages/bruno-js/src/runtime/test-runtime.js +++ b/packages/bruno-js/src/runtime/test-runtime.js @@ -32,6 +32,7 @@ const CryptoJS = require('crypto-js'); const NodeVault = require('node-vault'); const xml2js = require('xml2js'); const cheerio = require('cheerio'); +const tv4 = require('tv4'); const { executeQuickJsVmAsync } = require('../sandbox/quickjs'); const getResultsSummary = (results) => { @@ -209,6 +210,7 @@ class TestRuntime { 'crypto-js': CryptoJS, 'xml2js': xml2js, cheerio, + tv4, ...whitelistedModules, fs: allowScriptFilesystemAccess ? fs : undefined, 'node-vault': NodeVault diff --git a/packages/bruno-js/src/sandbox/bundle-libraries.js b/packages/bruno-js/src/sandbox/bundle-libraries.js index 30d6c7c03..1545ef5cd 100644 --- a/packages/bruno-js/src/sandbox/bundle-libraries.js +++ b/packages/bruno-js/src/sandbox/bundle-libraries.js @@ -12,6 +12,7 @@ const bundleLibraries = async () => { import btoa from "btoa"; import atob from "atob"; import * as CryptoJS from "@usebruno/crypto-js"; + import tv4 from "tv4"; globalThis.expect = expect; globalThis.assert = assert; globalThis.moment = moment; @@ -19,6 +20,7 @@ const bundleLibraries = async () => { globalThis.atob = atob; globalThis.Buffer = Buffer; globalThis.CryptoJS = CryptoJS; + globalThis.tv4 = tv4; globalThis.requireObject = { ...(globalThis.requireObject || {}), 'chai': { expect, assert }, @@ -26,7 +28,8 @@ const bundleLibraries = async () => { 'buffer': { Buffer }, 'btoa': btoa, 'atob': atob, - 'crypto-js': CryptoJS + 'crypto-js': CryptoJS, + 'tv4': tv4 }; `; diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 3914e6bfa..af4b13434 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -340,7 +340,8 @@ const folderRootSchema = Yup.object({ .nullable(), docs: Yup.string().nullable(), meta: Yup.object({ - name: Yup.string().nullable() + name: Yup.string().nullable(), + seq: Yup.number().min(1).nullable() }) .noUnknown(true) .strict() diff --git a/packages/bruno-tests/collection/bruno.json b/packages/bruno-tests/collection/bruno.json index ada36145a..d2aa0a97a 100644 --- a/packages/bruno-tests/collection/bruno.json +++ b/packages/bruno-tests/collection/bruno.json @@ -28,4 +28,4 @@ "requestType": "http", "requestUrl": "http://localhost:6000" } -} +} \ No newline at end of file diff --git a/packages/bruno-tests/collection/echo/echo headers.bru b/packages/bruno-tests/collection/echo/echo headers.bru new file mode 100644 index 000000000..9f6571109 --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo headers.bru @@ -0,0 +1,22 @@ +meta { + name: echo headers + type: http + seq: 13 +} + +post { + url: {{echo-host}} + body: none + auth: inherit +} + +headers { + Custom-Header-String: bruno +} + +tests { + test("test headers",function() { + expect(res.getHeaders()).to.have.property("Custom-Header-String".toLowerCase()) + expect(res.getHeaders()).to.have.property("Custom-Header-String".toLowerCase(), "bruno") + }) +} diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/array.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/array.bru new file mode 100644 index 000000000..7e3492abe --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/array.bru @@ -0,0 +1,45 @@ +meta { + name: array + type: http + seq: 6 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +script:post-response { + const obj = { + hello : "hello from post-res" + } + // Safe mode, Dev mode behaves differently, null is getting converted to undefined, although both have null in the response, tests with undefined fails in safe mode, this needs to be investigated,, undefined is not a valid JSON + res.setBody(["hello",1, null, undefined, true, obj]) +} + +tests { + test("res.setBody(array)", function() { + const body = res.getBody(); + expect(body.length).to.eql(6); + expect(body[0]).to.eql("hello") + expect(body[1]).to.eql(1) + expect(body[2]).to.be.null + // Safe mode, Dev mode behaves differently, null is getting converted to undefined, although both have null in the response, tests with undefined fails in safe mode, this needs to be investigated,, undefined is not a valid JSON + expect(body[3]).to.be.undefined; + expect(body[4]).to.eql(true) + expect(body[5].hello).to.eql("hello from post-res") + + }); + +} diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/boolean.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/boolean.bru new file mode 100644 index 000000000..28513d679 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/boolean.bru @@ -0,0 +1,33 @@ +meta { + name: boolean + type: http + seq: 7 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +script:post-response { + res.setBody(true) +} + +tests { + test("res.setBody(boolean)", function() { + const body = res.getBody(); + expect(body).to.be.true; + }); + +} diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/folder.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/folder.bru new file mode 100644 index 000000000..4e66417c7 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/folder.bru @@ -0,0 +1,3 @@ +meta { + name: setBody +} diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/null.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/null.bru new file mode 100644 index 000000000..8d8d2dcf5 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/null.bru @@ -0,0 +1,33 @@ +meta { + name: null + type: http + seq: 6 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +script:post-response { + res.setBody(null) +} + +tests { + test("res.setBody(null)", function() { + const body = res.getBody(); + expect(body).to.be.null; + }); + +} diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/number.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/number.bru new file mode 100644 index 000000000..a90aec802 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/number.bru @@ -0,0 +1,33 @@ +meta { + name: number + type: http + seq: 3 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +script:post-response { + res.setBody(2) +} + +tests { + test("res.setBody(number)", function() { + const body = res.getBody(); + expect(body).to.eql(2); + }); + +} diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/object.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/object.bru new file mode 100644 index 000000000..02ad78a2e --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/object.bru @@ -0,0 +1,35 @@ +meta { + name: object + type: http + seq: 1 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +script:post-response { + res.setBody({ + hello : "hello from post-res" + }) +} + +tests { + test("res.setBody(object)", function() { + const body = res.getBody(); + expect(body.hello).to.eql("hello from post-res"); + }); + +} diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/string.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/string.bru new file mode 100644 index 000000000..a87e57bd4 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/string.bru @@ -0,0 +1,33 @@ +meta { + name: string + type: http + seq: 4 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +script:post-response { + res.setBody("hello from post-res") +} + +tests { + test("res.setBody(string)", function() { + const body = res.getBody(); + expect(body).to.eql("hello from post-res"); + }); + +} diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/undefined.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/undefined.bru new file mode 100644 index 000000000..0dc918f1f --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/undefined.bru @@ -0,0 +1,36 @@ +meta { + name: undefined + type: http + seq: 7 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +script:post-response { + // if undefined is not passed to res.setBody() the test fails in only safe-mode, needs to check, undefined is not a valid JSON + // Safe mode, Dev mode behaves differently, null is getting converted to undefined, although both have null in the response, tests with undefined fails in safe mode, this needs to be investigated, undefined is not a valid JSON + res.setBody(undefined) +} + +tests { + test("res.setBody(undefined)", function() { + const body = res.getBody(); + // Safe mode, Dev mode behaves differently, null is getting converted to undefined, although both have null in the response, tests with undefined fails in safe mode, this needs to be investigated, undefined is not a valid JSON + expect(body).to.be.undefined; + }); + +} diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/tv4/folder.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/tv4/folder.bru new file mode 100644 index 000000000..4fd4bb1d2 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/inbuilt modules/tv4/folder.bru @@ -0,0 +1,3 @@ +meta { + name: tv4 +} diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/tv4/tv4.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/tv4/tv4.bru new file mode 100644 index 000000000..820a7a3b8 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/inbuilt modules/tv4/tv4.bru @@ -0,0 +1,39 @@ +meta { + name: tv4 + type: http + seq: 1 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: inherit +} + +body:json { + { + "name": "John", + "age": 30 + } +} + +tests { + const tv4 = require("tv4") + + const schema = { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'number' } + } + }; + + let responseData = res.getBody(); + + let isValid = tv4.validate(responseData, schema); + + test("Response body matches expected schema", function () { + expect(isValid, tv4.error ? tv4.error.message : "").to.be.true; + }); + +}