diff --git a/package-lock.json b/package-lock.json index 1f8eb1c..1977a28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,24 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@monaco-editor/react": "^4.7.0", "@tailwindcss/postcss": "^4.1.15", "copy-webpack-plugin": "^13.0.1", "electron-squirrel-startup": "^1.0.1", "lodash": "^4.17.21", + "monaco-editor": "^0.55.1", + "monaco-types": "^0.1.0", "pouchdb": "^9.0.0", "rc-field-form": "^2.7.0", "react": "^19.2.0", + "react-diff-view": "^3.3.2", + "react-diff-viewer": "^3.1.1", "react-dom": "^19.2.0", "react-router-dom": "^7.9.4", "react-simple-code-editor": "^0.14.1", "react-toastify": "^11.0.5", + "react-virtualized-auto-sizer": "^1.0.26", + "react-window": "^2.2.3", "sugar-high": "^0.9.4" }, "devDependencies": { @@ -32,9 +39,12 @@ "@electron-forge/plugin-fuses": "^7.10.2", "@electron-forge/plugin-webpack": "^7.10.2", "@electron/fuses": "^1.8.0", + "@types/diff": "^7.0.2", "@types/pouchdb": "^6.4.2", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", + "@types/react-virtualized-auto-sizer": "^1.0.4", + "@types/react-window": "^1.8.8", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "@vercel/webpack-asset-relocator-loader": "^1.7.3", @@ -70,7 +80,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -81,16 +90,77 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-identifier": { + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", @@ -100,6 +170,51 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@electron-forge/cli": { "version": "7.10.2", "resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-7.10.2.tgz", @@ -951,6 +1066,79 @@ "node": ">=14.14" } }, + "node_modules/@emotion/cache": { + "version": "10.0.29", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", + "integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==", + "license": "MIT", + "dependencies": { + "@emotion/sheet": "0.9.4", + "@emotion/stylis": "0.8.5", + "@emotion/utils": "0.11.3", + "@emotion/weak-memoize": "0.2.5" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "license": "MIT" + }, + "node_modules/@emotion/serialize": { + "version": "0.11.16", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", + "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/unitless": "0.7.5", + "@emotion/utils": "0.11.3", + "csstype": "^2.5.7" + } + }, + "node_modules/@emotion/serialize/node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "license": "MIT" + }, + "node_modules/@emotion/sheet": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz", + "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==", + "license": "MIT" + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@emotion/utils": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", + "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -1587,6 +1775,29 @@ "node": ">= 12.13.0" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2030,6 +2241,13 @@ "@types/ms": "*" } }, + "node_modules/@types/diff": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz", + "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -2207,7 +2425,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "dev": true, "license": "MIT" }, "node_modules/@types/pouchdb": { @@ -2441,6 +2658,26 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-virtualized-auto-sizer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.4.tgz", + "integrity": "sha512-nhYwlFiYa8M3S+O2T9QO/e1FQUYMr/wJENUdf/O0dhRi1RS/93rjrYQFYdbUqtdFySuhrtnEDX29P6eKOttY+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -2518,6 +2755,13 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", @@ -3424,6 +3668,75 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/babel-plugin-emotion": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz", + "integrity": "sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/serialize": "^0.11.16", + "babel-plugin-macros": "^2.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^1.0.5", + "find-root": "^1.1.0", + "source-map": "^0.5.7" + } + }, + "node_modules/babel-plugin-emotion/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/babel-plugin-emotion/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", + "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "cosmiconfig": "^6.0.0", + "resolve": "^1.12.0" + } + }, + "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3842,7 +4155,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3969,6 +4281,12 @@ "node": ">=6.0" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -4306,6 +4624,12 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", @@ -4422,6 +4746,18 @@ "node": ">=10" } }, + "node_modules/create-emotion": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-10.0.27.tgz", + "integrity": "sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg==", + "license": "MIT", + "dependencies": { + "@emotion/cache": "^10.0.27", + "@emotion/serialize": "^0.11.15", + "@emotion/sheet": "0.9.4", + "@emotion/utils": "0.11.3" + } + }, "node_modules/cross-dirname": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", @@ -4622,7 +4958,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4815,6 +5150,21 @@ "dev": true, "license": "MIT" }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "license": "Apache-2.0" + }, "node_modules/dir-compare": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", @@ -4919,6 +5269,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -5592,6 +5951,16 @@ "node": ">= 4" } }, + "node_modules/emotion": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/emotion/-/emotion-10.0.27.tgz", + "integrity": "sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g==", + "license": "MIT", + "dependencies": { + "babel-plugin-emotion": "^10.0.27", + "create-emotion": "^10.0.27" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -5715,7 +6084,6 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -6606,6 +6974,12 @@ "dev": true, "license": "MIT" }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6836,7 +7210,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7038,6 +7411,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gitdiff-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/gitdiff-parser/-/gitdiff-parser-0.3.1.tgz", + "integrity": "sha512-YQJnY8aew65id8okGxKCksH3efDCJ9HzV7M9rsvd65habf39Pkh4cgYJ27AaoDMqo1X98pgNJhNMrm/kpV7UVQ==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -7310,7 +7689,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7705,7 +8083,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -7840,7 +8217,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, "license": "MIT" }, "node_modules/is-async-function": { @@ -7949,7 +8325,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -8451,7 +8826,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -8467,6 +8841,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -9075,7 +9461,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/listr2": { @@ -9256,6 +9641,18 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -9342,6 +9739,18 @@ "node": ">=6" } }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -9404,6 +9813,12 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -9648,11 +10063,29 @@ "node": ">=10" } }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, + "node_modules/monaco-types": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/monaco-types/-/monaco-types-0.1.0.tgz", + "integrity": "sha512-aWK7SN9hAqNYi0WosPoMjenMeXJjwCxDibOqWffyQ/qXdzB/86xshGQobRferfmNz7BSNQ8GB0MD0oby9/5fTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, "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/multicast-dns": { @@ -9946,6 +10379,15 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -10351,7 +10793,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -10377,7 +10818,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -10447,7 +10887,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-to-regexp": { @@ -10461,7 +10900,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10907,6 +11345,23 @@ "node": ">=10" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -11090,6 +11545,44 @@ "node": ">=0.10.0" } }, + "node_modules/react-diff-view": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/react-diff-view/-/react-diff-view-3.3.2.tgz", + "integrity": "sha512-wPVq4ktTcGOHbhnWKU/gHLtd3N2Xd+OZ/XQWcKA06dsxlSsESePAumQILwHtiak2nMCMiWcIfBpqZ5OiharUPA==", + "license": "MIT", + "dependencies": { + "classnames": "^2.3.2", + "diff-match-patch": "^1.0.5", + "gitdiff-parser": "^0.3.1", + "lodash": "^4.17.21", + "shallow-equal": "^3.1.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0" + } + }, + "node_modules/react-diff-viewer": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/react-diff-viewer/-/react-diff-viewer-3.1.1.tgz", + "integrity": "sha512-rmvwNdcClp6ZWdS11m1m01UnBA4OwYaLG/li0dB781e/bQEzsGyj+qewVd6W5ztBwseQ72pO7nwaCcq5jnlzcw==", + "license": "MIT", + "dependencies": { + "classnames": "^2.2.6", + "create-emotion": "^10.0.14", + "diff": "^4.0.1", + "emotion": "^10.0.14", + "memoize-one": "^5.0.4", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">= 8" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0", + "react-dom": "^15.3.0 || ^16.0.0" + } + }, "node_modules/react-dom": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", @@ -11178,6 +11671,26 @@ "react-dom": "^18 || ^19" } }, + "node_modules/react-virtualized-auto-sizer": { + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz", + "integrity": "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A==", + "license": "MIT", + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-window": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.3.tgz", + "integrity": "sha512-gTRqQYC8ojbiXyd9duYFiSn2TJw0ROXCgYjenOvNKITWzK0m0eCvkUsEUM08xvydkMh7ncp+LE0uS3DeNGZxnQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/read-binary-file-arch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", @@ -11451,7 +11964,6 @@ "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -11479,7 +11991,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -12021,6 +12532,12 @@ "node": ">=8" } }, + "node_modules/shallow-equal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz", + "integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -12356,6 +12873,12 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -12627,7 +13150,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13567,6 +14089,15 @@ "integrity": "sha512-Tm7jR1xTzBbPW+6y1tknKiEhz04Wf/1iZkcTJjSFcpNko43+dFW6+OOeQe9taJIug3NdfUAjFKgUSyQrIKaDvQ==", "license": "Apache-2.0" }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", @@ -14291,7 +14822,6 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, "license": "ISC", "engines": { "node": ">= 6" diff --git a/package.json b/package.json index 810a177..2f38547 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,12 @@ "@electron-forge/plugin-fuses": "^7.10.2", "@electron-forge/plugin-webpack": "^7.10.2", "@electron/fuses": "^1.8.0", + "@types/diff": "^7.0.2", "@types/pouchdb": "^6.4.2", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", + "@types/react-virtualized-auto-sizer": "^1.0.4", + "@types/react-window": "^1.8.8", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "@vercel/webpack-asset-relocator-loader": "^1.7.3", @@ -49,17 +52,24 @@ "typescript": "~4.5.4" }, "dependencies": { + "@monaco-editor/react": "^4.7.0", "@tailwindcss/postcss": "^4.1.15", "copy-webpack-plugin": "^13.0.1", "electron-squirrel-startup": "^1.0.1", "lodash": "^4.17.21", + "monaco-editor": "^0.55.1", + "monaco-types": "^0.1.0", "pouchdb": "^9.0.0", "rc-field-form": "^2.7.0", "react": "^19.2.0", + "react-diff-view": "^3.3.2", + "react-diff-viewer": "^3.1.1", "react-dom": "^19.2.0", "react-router-dom": "^7.9.4", "react-simple-code-editor": "^0.14.1", "react-toastify": "^11.0.5", + "react-virtualized-auto-sizer": "^1.0.26", + "react-window": "^2.2.3", "sugar-high": "^0.9.4" } } diff --git a/src/channelHandlers/browserstack-api.ts b/src/channelHandlers/browserstack-api.ts index a659451..913e884 100644 --- a/src/channelHandlers/browserstack-api.ts +++ b/src/channelHandlers/browserstack-api.ts @@ -228,3 +228,37 @@ export const getAutomateParsedSeleniumLogs = async (session: AutomateSessionResp return result; } + +export const getSeleniumLogs = async (selenium_logs_url: string) => { + if (!selenium_logs_url) { + return 'No Selenium logs available for this session'; + } + try { + const response = await fetch(selenium_logs_url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.text(); + // For other URLs, use the existing download function with auth + return await download(selenium_logs_url); + } catch (error) { + console.error('Failed to fetch Selenium logs:', error); + return 'Failed to load Selenium logs'; + } +} + +export const getHarLogs = async (harLogsUrl: string) => { + if (!harLogsUrl) { + return 'No network logs available for this session'; + } + try { + const response = await fetch(harLogsUrl); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.text(); + } catch (error) { + console.error('Failed to fetch HAR logs:', error); + return 'Failed to load network logs'; + } +} diff --git a/src/constants/ipc-channels.ts b/src/constants/ipc-channels.ts index e2a49cb..4e3b5e8 100644 --- a/src/constants/ipc-channels.ts +++ b/src/constants/ipc-channels.ts @@ -11,7 +11,9 @@ const CHANNELS = { BROWSERSTACK_EXECUTE_SESSION_COMMAND:'BROWSERSTACK_EXECUTE_SESSION_COMMAND', ELECTRON_OPEN_URL:'ELECTRON_OPEN_URL', GET_BROWSERSTACK_AUTOMATE_PARSED_SESSION_LOGS:'GET_BROWSERSTACK_AUTOMATE_PARSED_SESSION_LOGS', - GET_BROWSERSTACK_AUTOMATE_PARSED_SELENIUM_LOGS:'GET_BROWSERSTACK_AUTOMATE_PARSED_SELENIUM_LOGS' + GET_BROWSERSTACK_AUTOMATE_PARSED_SELENIUM_LOGS:'GET_BROWSERSTACK_AUTOMATE_PARSED_SELENIUM_LOGS', + GET_BROWSERSTACK_AUTOMATE_SELENIUM_LOGS: 'GET /automate/sessions/seleniumLogs', + GET_BROWSERSTACK_AUTOMATE_HAR_LOGS: 'GET /automate/sessions/harLogs' } export default CHANNELS \ No newline at end of file diff --git a/src/global.d.ts b/src/global.d.ts index e30ba7f..9138765 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -14,11 +14,15 @@ declare global { stopSession: (options: StopSessionOptions) => Promise executeCommand: (options: ExecuteCommandOptions) => any getAutomateParsedSessionLogs: (session: AutomateSessionResponse) => Promise - getAutomateParsedSeleniumLogs: (session: AutomateSessionResponse) => Promise + getAutomateParsedSeleniumLogs: (session: AutomateSessionResponse) => Promise, + getAutomateParsedTextLogs: (session:AutomateSessionResponse) => Promise + getSeleniumLogs: (selenium_logs_url: string) => Promise + getHarLogs: (harLogsUrl: string) => Promise } type ElectronAPI = { openExternalUrl: (url: string) => Promise + } interface DBItem { @@ -41,6 +45,7 @@ declare global { title: string description: string, path: string + component: React.ReactNode | null }[] } diff --git a/src/index.ts b/src/index.ts index f77959e..713d6d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,10 +8,10 @@ import StorageKeys from './constants/storage-keys'; import CONFIG from './constants/config'; import { mkdirSync } from 'fs' -import { executeCommand, getAutomateSessionDetails, getParsedAutomateTextLogs, startBrowserStackSession, stopBrowserStackSession, getAutomateParsedSeleniumLogs, getAutomateParsedSessionLogs, } from './channelHandlers/browserstack-api'; +import { executeCommand, getAutomateSessionDetails, getParsedAutomateTextLogs, startBrowserStackSession, stopBrowserStackSession, getAutomateParsedSeleniumLogs, getAutomateParsedSessionLogs,getSeleniumLogs, getHarLogs } from './channelHandlers/browserstack-api'; import { openExternalUrl } from './channelHandlers/electron-api'; -import { get } from 'http'; + // This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack // plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on // whether you're running in development or production). @@ -101,6 +101,8 @@ app.whenReady().then(() => { ipcMain.handle(CHANNELS.BROWSERSTACK_STOP_SESSION, (_, options) => stopBrowserStackSession(options)) ipcMain.handle(CHANNELS.BROWSERSTACK_EXECUTE_SESSION_COMMAND, (_, options) => executeCommand(options)) ipcMain.handle(CHANNELS.ELECTRON_OPEN_URL, (_, url) => openExternalUrl(url)) + ipcMain.handle(CHANNELS.GET_BROWSERSTACK_AUTOMATE_SELENIUM_LOGS,(_, selenium_logs_url) => getSeleniumLogs(selenium_logs_url)); + ipcMain.handle(CHANNELS.GET_BROWSERSTACK_AUTOMATE_HAR_LOGS, (_, har_logs_url) => getHarLogs(har_logs_url)); }); // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and import them here. diff --git a/src/preload.ts b/src/preload.ts index a4fe37c..89056b7 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -17,6 +17,8 @@ const browserstackAPI: BrowserStackAPI = { executeCommand: (options) => ipcRenderer.invoke(CHANNELS.BROWSERSTACK_EXECUTE_SESSION_COMMAND, options), getAutomateParsedSessionLogs: (session)=>ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_AUTOMATE_PARSED_SESSION_LOGS,session), getAutomateParsedSeleniumLogs: (session)=>ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_AUTOMATE_PARSED_SELENIUM_LOGS,session), + getSeleniumLogs: (selenium_logs_url) => ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_AUTOMATE_SELENIUM_LOGS, selenium_logs_url), + getHarLogs: (har_logs_url) => ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_AUTOMATE_HAR_LOGS, har_logs_url) } const electronAPI: ElectronAPI = { diff --git a/src/renderer/components/sidebar.tsx b/src/renderer/components/sidebar.tsx index 0272eb0..db43715 100644 --- a/src/renderer/components/sidebar.tsx +++ b/src/renderer/components/sidebar.tsx @@ -7,24 +7,6 @@ const TopMenu = [ path: "/", }, ]; -const ProductsMenu = [ - { - title: "Automate", - path: "/automate", - }, - { - title: "App Automate", - path: "/app-automate", - }, - { - title: "Percy", - path: "/percy", - }, - { - title: "Accessibility", - path: "/accessibility", - }, -]; export default function Sidebar() { const location = useLocation(); diff --git a/src/renderer/index.css b/src/renderer/index.css index df2ff97..8760d7d 100644 --- a/src/renderer/index.css +++ b/src/renderer/index.css @@ -11,4 +11,41 @@ --sh-string: #00a99a; --sh-keyword: #f47067; --sh-comment: #a19595; -} \ No newline at end of file +} + +.diff-container .diff-viewer-code-column { + max-width: 600px; + overflow-wrap: anywhere; +} + +.diff-container .rdv-code-wrapper { + max-width: 25vw; + white-space: pre-wrap !important; + word-break: break-word !important; +} + +.diff-container .rdv-code { + white-space: pre-wrap !important; + word-break: break-word !important; +} + +/* Force wrapping in ReactDiffViewer */ +.react-diff-viewer, +.react-diff-viewer * { + white-space: pre-wrap !important; + word-break: break-word !important; +} + +/* Limit each side to half of the container */ +.react-diff-viewer .diff-viewer-container, +.react-diff-viewer .diff-viewer-split { + max-width: 48vw !important; +} + +/* Override left + right code column width */ +.react-diff-viewer .diff-code, +.react-diff-viewer .diff-line { + max-width: 48vw !important; + width: 48vw !important; + display: block !important; +} diff --git a/src/renderer/products.ts b/src/renderer/products.ts index ff76a69..e8f6b35 100644 --- a/src/renderer/products.ts +++ b/src/renderer/products.ts @@ -1,6 +1,7 @@ import AutomatePage from "./routes/automate"; import ReplayTool from "./routes/automate/tools/replay-tool"; import LatencyFinder from "./routes/automate/tools/latency-finder"; +import SessionComparison from "./routes/automate/tools/session-comparison"; const Products = [ { @@ -21,6 +22,83 @@ const Products = [ path: "/automate/latency-analyser", component: LatencyFinder, }, + { + title: "Session Comparison", + description: "Compares logs across sessions and highlights differences", + path: '/automate/session-comparison', + component: SessionComparison + } + ], + }, + { + name: "App Automate", + path: "/app-automate", + page: AutomatePage, + tools: [ + { + title: "Replay Toolkit", + description: "Replays the sessions on BrowserStack by parsing Raw Logs", + path: "/automate/replay-toolkit", + component: null, + }, + { + title: "Latency Analyser", + description: + "Analyses time spend on different actions. Helpful to identify inside/outside time for a customer session.", + path: "/automate/latency-analyser", + component: null, + }, + { + title: "Session Comparison", + description: "Compares logs across sessions and highlights differences", + path: '/automate/session-comparison', + component: null + } + ], + }, + { + name: "Percy", + path: "/percy", + page: AutomatePage, + tools: [ + { + title: "Snapshot Replay", + description: "Replay snapshots", + path: "/percy/snapshot-replay", + component: null, + }, + { + title: "CLI Logs Downloader", + description: "Download CLI logs using hash ID displayed in Customer's console", + path: "/percy/cli-log-downloader", + component: null, + }, + ], + }, + { + name: "Test Report & Analytics", + path: "/tra", + page: AutomatePage, + tools: [ + { + title: "SDK Logs Downloader", + description: "Download SDK logs from Backend", + path: "/tra/download-logs", + component: null, + }, + ], + }, + { + name: "Web Accessibility", + path: "/web-accessibility", + page: AutomatePage, + tools: [ + { + title: "Automate Session Finder", + description: "Find associated automate session for accessibility scanner run", + path: "/web-a11y/session-finder", + component: null, + }, ], }, ]; diff --git a/src/renderer/routes/automate/index.tsx b/src/renderer/routes/automate/index.tsx index 57b1647..c4f14eb 100644 --- a/src/renderer/routes/automate/index.tsx +++ b/src/renderer/routes/automate/index.tsx @@ -2,22 +2,42 @@ import { NavLink } from "react-router-dom" export default function AutomatePage(props: ProductPageProps) { const { tools } = props + return (
{tools.map((tool) => { - return ( - -
-
-

{tool.title}

-

{tool.description}

+ const isComingSoon = tool.component === null + + const Card = ( +
+
+

+ {tool.title} +

+

{tool.description}

+
+ {isComingSoon && ( + + Coming soon + + )}
+
+ ) + + return isComingSoon ? ( +
+ {Card} +
+ ) : ( + + {Card} ) })}
) -} \ No newline at end of file +} diff --git a/src/renderer/routes/automate/tools/session-comparison.tsx b/src/renderer/routes/automate/tools/session-comparison.tsx new file mode 100644 index 0000000..442b47c --- /dev/null +++ b/src/renderer/routes/automate/tools/session-comparison.tsx @@ -0,0 +1 @@ +export { default } from './session-comparison/index'; \ No newline at end of file diff --git a/src/renderer/routes/automate/tools/session-comparison/components/DiffViewer.tsx b/src/renderer/routes/automate/tools/session-comparison/components/DiffViewer.tsx new file mode 100644 index 0000000..2a08017 --- /dev/null +++ b/src/renderer/routes/automate/tools/session-comparison/components/DiffViewer.tsx @@ -0,0 +1,178 @@ +import { useMemo, useState, useEffect } from 'react'; +import { DiffLine } from '../types'; +import { generateDiff } from '../utils/diffAlgorithm'; + +interface DiffViewerProps { + oldValue: string; + newValue: string; + leftTitle: string; + rightTitle: string; + batchSize?: number; +} + +function LineContent({ line, side }: { line: DiffLine; side: 'left' | 'right' }) { + const content = side === 'left' ? line.leftLine : line.rightLine; + + if (content === null) return ; + if (content === '') return ; + + if (line.type === 'modified' && line.charDiffs) { + return ( + + {line.charDiffs.map((diff, idx) => { + if (diff.type === 'common') { + return {diff.text}; + } else if (diff.type === 'removed' && side === 'left') { + return ( + + {diff.text} + + ); + } else if (diff.type === 'added' && side === 'right') { + return ( + + {diff.text} + + ); + } + return null; + })} + + ); + } + + return {content}; +} + +export default function DiffViewer({ + oldValue, + newValue, + leftTitle, + rightTitle, + batchSize = 200 +}: DiffViewerProps) { + const allDiffLines = useMemo(() => { + return generateDiff(oldValue, newValue); + }, [oldValue, newValue]); + + const [renderLimit, setRenderLimit] = useState(batchSize); + + useEffect(() => { + setRenderLimit(batchSize); + }, [oldValue, newValue, batchSize]); + + const visibleLines = allDiffLines.slice(0, renderLimit); + const remainingLines = allDiffLines.length - renderLimit; + + const handleLoadMore = () => { + setRenderLimit((prev) => Math.min(prev + batchSize, allDiffLines.length)); + }; + + const handleLoadAll = () => { + setRenderLimit(allDiffLines.length); + }; + + const getLineStyle = (type: DiffLine['type'], side: 'left' | 'right') => { + if (type === 'unchanged') return 'bg-white'; + if (type === 'removed' && side === 'left') return 'bg-red-50'; + if (type === 'added' && side === 'right') return 'bg-green-50'; + if (type === 'modified') return side === 'left' ? 'bg-orange-50' : 'bg-blue-50'; + if (type === 'removed' && side === 'right') return 'bg-gray-50'; + if (type === 'added' && side === 'left') return 'bg-gray-50'; + return 'bg-white'; + }; + + const getIndicator = (type: DiffLine['type'], side: 'left' | 'right') => { + if (type === 'unchanged') return '•'; + if (type === 'removed' && side === 'left') return '−'; + if (type === 'added' && side === 'right') return '+'; + if (type === 'modified') return '~'; + return '•'; + }; + + return ( +
+
+
+ {leftTitle} +
+
+ {rightTitle} +
+
+ +
+ {visibleLines.map((line, idx) => ( +
+
+
+ {line.leftNumber || ''} +
+
+ {getIndicator(line.type, 'left')} +
+
+ +
+
+ +
+
+ {line.rightNumber || ''} +
+
+ {getIndicator(line.type, 'right')} +
+
+ +
+
+
+ ))} + + {remainingLines > 0 && ( +
+
+ Showing {renderLimit.toLocaleString()} of {allDiffLines.length.toLocaleString()} lines. +
+
+ + {remainingLines < 5000 && ( + + )} +
+
+ )} +
+ +
+
+ + Removed + + + ~ Modified + +
+
+ + + Added + + + ~ Modified + +
+
+
+ ); +} diff --git a/src/renderer/routes/automate/tools/session-comparison/components/SessionDiffView.tsx b/src/renderer/routes/automate/tools/session-comparison/components/SessionDiffView.tsx new file mode 100644 index 0000000..69e62bc --- /dev/null +++ b/src/renderer/routes/automate/tools/session-comparison/components/SessionDiffView.tsx @@ -0,0 +1,287 @@ +import { useState, useMemo } from 'react'; +import DiffViewer from './DiffViewer'; +import { SessionData, TextLogsResult } from '../types'; +import { compareNetworkLogs, formatNetworkEntry } from '../utils/networkParser'; + +interface SessionDiffViewProps { + sessionA: SessionData; + sessionB: SessionData; + logsA: TextLogsResult; + logsB: TextLogsResult; + seleniumA: string | null; + seleniumB: string | null; + harA: string | null; + harB: string | null; + loadingA: boolean; + loadingB: boolean; + loadingHarA: boolean; + loadingHarB: boolean; +} + +function createInfoString(session: SessionData) { + if (!session?.automation_session) return "No Session Data"; + const s = session.automation_session; + return `Project: ${s.project_name} +Build: ${s.build_name} +Status: ${s.status} +OS: ${s.os} ${s.os_version} +Browser: ${s.browser} ${s.browser_version} +Device: ${s.device || "N/A"} +Duration: ${s.duration}s +Created At: ${new Date(s.created_at).toLocaleString()}`.trim(); +} + +export default function SessionDiffView({ + sessionA, + sessionB, + logsA, + logsB, + seleniumA, + seleniumB, + harA, + harB, + loadingA, + loadingB, + loadingHarA, + loadingHarB +}: SessionDiffViewProps) { + const [activeTab, setActiveTab] = useState<'info' | 'capabilities' | 'selenium' | 'network'>('info'); + const [seleniumBatch, setSeleniumBatch] = useState(0); + const LINES_PER_BATCH = 200; + + const networkComparison = useMemo(() => { + return compareNetworkLogs(harA, harB); + }, [harA, harB]); + + const seleniumLogStats = useMemo(() => { + const logsA = seleniumA || ""; + const logsB = seleniumB || ""; + const linesA = logsA.split('\n'); + const linesB = logsB.split('\n'); + const maxLines = Math.max(linesA.length, linesB.length); + const totalBatches = Math.ceil(maxLines / LINES_PER_BATCH); + + return { + linesA: linesA.length, + linesB: linesB.length, + maxLines, + totalBatches, + linesA_raw: linesA, + linesB_raw: linesB + }; + }, [seleniumA, seleniumB]); + + const currentSeleniumBatch = useMemo(() => { + const { linesA_raw, linesB_raw } = seleniumLogStats; + const startLine = seleniumBatch * LINES_PER_BATCH; + const endLine = startLine + LINES_PER_BATCH; + + const batchA = linesA_raw.slice(startLine, endLine).join('\n'); + const batchB = linesB_raw.slice(startLine, endLine).join('\n'); + + return { batchA, batchB, startLine: startLine + 1, endLine: Math.min(endLine, seleniumLogStats.maxLines) }; + }, [seleniumBatch, seleniumLogStats]); + + const infoStrA = createInfoString(sessionA); + const infoStrB = createInfoString(sessionB); + const nameA = sessionA?.automation_session?.name || "Unnamed Session A"; + const nameB = sessionB?.automation_session?.name || "Unnamed Session B"; + const capsStrA = JSON.stringify(logsA?.capabilities?.[0] || {}, null, 2); + const capsStrB = JSON.stringify(logsB?.capabilities?.[0] || {}, null, 2); + + const tabs = [ + { id: 'info', label: 'Session Info' }, + { id: 'capabilities', label: 'Capabilities' }, + // { id: 'selenium', label: 'Selenium Logs' }, + { id: 'network', label: 'Network Logs' } + ] as const; + + const handleTabChange = (tabId: typeof activeTab) => { + setActiveTab(tabId); + if (tabId === 'selenium') { + setSeleniumBatch(0); + } + }; + + return ( +
+
+ {tabs.map((tab) => ( + + ))} +
+ +
+ {activeTab === 'info' && ( + + )} + {activeTab === 'capabilities' && ( + + )} + {/* {activeTab === 'selenium' && ( +
+ {!loadingA && !loadingB && seleniumLogStats.totalBatches > 1 && ( +
+
+ + Lines {currentSeleniumBatch.startLine} - {currentSeleniumBatch.endLine} of {seleniumLogStats.maxLines} + + + (Batch {seleniumBatch + 1} of {seleniumLogStats.totalBatches}) + +
+
+ + +
+
+ )} + +
+ +
+
+ )} */} + {activeTab === 'network' && ( +
+ {loadingHarA || loadingHarB ? ( +
+ Loading network logs... +
+ ) : ( + <> + {networkComparison.matched.length > 0 && ( +
+

+ Matched Requests ({networkComparison.matched.length}) +

+ {networkComparison.matched.map((item, idx) => { + const [method, url] = item.key.split('|||'); + const urlObj = new URL(url); + const shortUrl = urlObj.origin + urlObj.pathname + urlObj.search; + return ( +
+
+ {method} {shortUrl} +
+
+
+
{nameA}
+
+                                {formatNetworkEntry(item.key, item.dataA)}
+                              
+
+
+
{nameB}
+
+                                {formatNetworkEntry(item.key, item.dataB)}
+                              
+
+
+
+ ); + })} +
+ )} + {networkComparison.onlyInA.length > 0 && ( +
+

+ Only in {nameA} ({networkComparison.onlyInA.length}) +

+ {networkComparison.onlyInA.map((item, idx) => { + const [method, url] = item.key.split('|||'); + const urlObj = new URL(url); + const shortUrl = urlObj.pathname + urlObj.search; + return ( +
+
+ {method} {shortUrl} +
+
+                            {formatNetworkEntry(item.key, item.data)}
+                          
+
+ ); + })} +
+ )} + {networkComparison.onlyInB.length > 0 && ( +
+

+ Only in {nameB} ({networkComparison.onlyInB.length}) +

+ {networkComparison.onlyInB.map((item, idx) => { + const [method, url] = item.key.split('|||'); + const urlObj = new URL(url); + const shortUrl = urlObj.pathname + urlObj.search; + return ( +
+
+ {method} {shortUrl} +
+
+                            {formatNetworkEntry(item.key, item.data)}
+                          
+
+ ); + })} +
+ )} + {networkComparison.matched.length === 0 && + networkComparison.onlyInA.length === 0 && + networkComparison.onlyInB.length === 0 && ( +
+ No network logs available for either session +
+ )} + + )} +
+ )} +
+
+ ); +} diff --git a/src/renderer/routes/automate/tools/session-comparison/index.tsx b/src/renderer/routes/automate/tools/session-comparison/index.tsx new file mode 100644 index 0000000..33a304f --- /dev/null +++ b/src/renderer/routes/automate/tools/session-comparison/index.tsx @@ -0,0 +1,145 @@ +import Form from "rc-field-form"; +import { usePromise } from "../../../../hooks/use-promise"; +import { toast } from "react-toastify"; +import { useState } from "react"; +import SessionDiffView from './components/SessionDiffView'; +import { sanitizeCapabilities, sanitizeTextLogs } from './utils/sanitization'; + +const { Field } = Form; + +export default function SessionComparison() { + const [fetchSessionDetails] = usePromise(window.browserstackAPI.getAutomateSessionDetails); + const [parseTextLogs] = usePromise(window.browserstackAPI.getAutomateParsedTextLogs); + + const [session, setSession] = useState(null); + const [textLogsResult, setTextLogsResult] = useState(null); + const [seleniumLogsA, setSeleniumLogsA] = useState(null); + const [loadingSeleniumA, setLoadingSeleniumA] = useState(false); + + const [session2, setSession2] = useState(null); + const [textLogsResult2, setTextLogsResult2] = useState(null); + const [seleniumLogsB, setSeleniumLogsB] = useState(null); + const [loadingSeleniumB, setLoadingSeleniumB] = useState(false); + + const [harLogsA, setHarLogsA] = useState(null); + const [harLogsB, setHarLogsB] = useState(null); + const [loadingHarA, setLoadingHarA] = useState(false); + const [loadingHarB, setLoadingHarB] = useState(false); + + const loadSessionA = async (sessionId: string) => { + try { + const rawSessionA = await fetchSessionDetails(sessionId); + const sessionA = sanitizeCapabilities(rawSessionA); + const rawLogsA = await parseTextLogs(rawSessionA); + const logsA = sanitizeTextLogs(rawLogsA); + setSession(sessionA); + setTextLogsResult(logsA); + + setLoadingSeleniumA(true); + const selA = await window.browserstackAPI.getSeleniumLogs(rawSessionA.automation_session.selenium_logs_url); + setSeleniumLogsA(selA); + setLoadingSeleniumA(false); + + setLoadingHarA(true); + const harA = await window.browserstackAPI.getHarLogs(rawSessionA.automation_session.har_logs_url); + setHarLogsA(harA); + setLoadingHarA(false); + } catch (err) { + console.error("Error in loadSessionA:", err); + setLoadingSeleniumA(false); + setLoadingHarA(false); + throw new Error("Failed to load Session A"); + } + }; + + const loadSessionB = async (sessionId: string) => { + try { + const rawSessionB = await fetchSessionDetails(sessionId); + const sessionB = sanitizeCapabilities(rawSessionB); + const rawLogsB = await parseTextLogs(rawSessionB); + const logsB = sanitizeTextLogs(rawLogsB); + setSession2(sessionB); + setTextLogsResult2(logsB); + + setLoadingSeleniumB(true); + const selB = await window.browserstackAPI.getSeleniumLogs(rawSessionB.automation_session.selenium_logs_url); + setSeleniumLogsB(selB); + setLoadingSeleniumB(false); + + setLoadingHarB(true); + const harB = await window.browserstackAPI.getHarLogs(rawSessionB.automation_session.har_logs_url); + setHarLogsB(harB); + setLoadingHarB(false); + } catch (err) { + console.error("Error in loadSessionB:", err); + setLoadingSeleniumB(false); + setLoadingHarB(false); + throw new Error("Failed to load Session B"); + } + }; + + const OpenSession = (input: any) => { + setSession(null); + setTextLogsResult(null); + setSeleniumLogsA(null); + setSession2(null); + setTextLogsResult2(null); + setSeleniumLogsB(null); + + toast.promise( + Promise.all([ + loadSessionA(input.sessionIdA), + loadSessionB(input.sessionIdB) + ]), + { + pending: "Opening Sessions...", + success: "Sessions Loaded", + error: { + render({ data }) { + console.error(data); + return "Failed to load one or more sessions. Check console."; + }, + }, + } + ); + }; + + return ( +
+

Session Comparison

+ +
+ + + + + + + + + +
+ +
+ {session && session2 && textLogsResult && textLogsResult2 && ( + + )} +
+
+ ); +} diff --git a/src/renderer/routes/automate/tools/session-comparison/types.ts b/src/renderer/routes/automate/tools/session-comparison/types.ts new file mode 100644 index 0000000..5d1ceb1 --- /dev/null +++ b/src/renderer/routes/automate/tools/session-comparison/types.ts @@ -0,0 +1,65 @@ +export interface CharDiff { + type: 'common' | 'removed' | 'added'; + text: string; +} + +export interface DiffLine { + type: 'unchanged' | 'removed' | 'added' | 'modified'; + leftLine: string | null; + rightLine: string | null; + leftNumber: number | null; + rightNumber: number | null; + charDiffs?: CharDiff[]; +} + +export interface SessionData { + automation_session: { + name?: string; + project_name: string; + build_name: string; + status: string; + os: string; + os_version: string; + browser: string; + browser_version: string; + device?: string; + duration: number; + created_at: string; + selenium_logs_url: string; + har_logs_url: string; + }; + [key: string]: any; +} + +export interface TextLogsResult { + capabilities: any[]; + [key: string]: any; +} + +export interface NetworkEntry { + request: { + method: string; + url: string; + }; + response: { + status: number; + bodySize?: number; + content?: { + mimeType?: string; + }; + }; + time?: number; + timings?: Record; +} + +export interface HarLog { + log: { + entries: NetworkEntry[]; + }; +} + +export interface NetworkComparison { + matched: Array<{ key: string; dataA: any[]; dataB: any[] }>; + onlyInA: Array<{ key: string; data: any[] }>; + onlyInB: Array<{ key: string; data: any[] }>; +} diff --git a/src/renderer/routes/automate/tools/session-comparison/utils/diffAlgorithm.ts b/src/renderer/routes/automate/tools/session-comparison/utils/diffAlgorithm.ts new file mode 100644 index 0000000..ebf6303 --- /dev/null +++ b/src/renderer/routes/automate/tools/session-comparison/utils/diffAlgorithm.ts @@ -0,0 +1,278 @@ +import { CharDiff, DiffLine } from '../types'; + +export function generateCharDiffs(oldText: string, newText: string): CharDiff[] { + const diffs: CharDiff[] = []; + let i = 0, j = 0; + + while (i < oldText.length || j < newText.length) { + let commonStart = 0; + while (i + commonStart < oldText.length && + j + commonStart < newText.length && + oldText[i + commonStart] === newText[j + commonStart]) { + commonStart++; + } + + if (commonStart > 0) { + diffs.push({ type: 'common', text: oldText.slice(i, i + commonStart) }); + i += commonStart; + j += commonStart; + continue; + } + + let oldNext = i; + let newNext = j; + let found = false; + + for (let lookAhead = 1; lookAhead <= Math.min(50, Math.max(oldText.length - i, newText.length - j)); lookAhead++) { + if (i + lookAhead < oldText.length && j < newText.length && oldText[i + lookAhead] === newText[j]) { + oldNext = i + lookAhead; + newNext = j; + found = true; + break; + } + if (j + lookAhead < newText.length && i < oldText.length && newText[j + lookAhead] === oldText[i]) { + oldNext = i; + newNext = j + lookAhead; + found = true; + break; + } + } + + if (!found) { + oldNext = Math.min(i + 1, oldText.length); + newNext = Math.min(j + 1, newText.length); + } + + if (i < oldNext) { + diffs.push({ type: 'removed', text: oldText.slice(i, oldNext) }); + i = oldNext; + } + if (j < newNext) { + diffs.push({ type: 'added', text: newText.slice(j, newNext) }); + j = newNext; + } + } + + return diffs; +} + +function calculateSimilarity(str1: string, str2: string): number { + if (!str1 || !str2) return 0; + + const longer = str1.length > str2.length ? str1 : str2; + const shorter = str1.length > str2.length ? str2 : str1; + + if (longer.length === 0) return 1.0; + + const words1 = str1.split(/\s+/); + const words2 = str2.split(/\s+/); + const commonWords = words1.filter(w => words2.includes(w)).length; + const totalWords = Math.max(words1.length, words2.length); + const wordSimilarity = commonWords / totalWords; + + const editDistance = levenshteinDistance(str1, str2); + const charSimilarity = (longer.length - editDistance) / longer.length; + + return wordSimilarity * 0.7 + charSimilarity * 0.3; +} + +function levenshteinDistance(str1: string, str2: string): number { + const matrix = []; + + for (let i = 0; i <= str2.length; i++) { + matrix[i] = [i]; + } + + for (let j = 0; j <= str1.length; j++) { + matrix[0][j] = j; + } + + for (let i = 1; i <= str2.length; i++) { + for (let j = 1; j <= str1.length; j++) { + if (str2.charAt(i - 1) === str1.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, + matrix[i][j - 1] + 1, + matrix[i - 1][j] + 1 + ); + } + } + } + + return matrix[str2.length][str1.length]; +} + +export function generateDiff(oldValue: string, newValue: string): DiffLine[] { + const oldLines = oldValue.split('\n'); + const newLines = newValue.split('\n'); + const result: DiffLine[] = []; + + let leftNum = 1; + let rightNum = 1; + let i = 0; + let j = 0; + + const getLookaheadMatch = (oldIdx: number, newIdx: number, range: number = 10) => { + let bestMatch = { oldIdx: -1, newIdx: -1, similarity: 0.3 }; + + for (let oi = oldIdx; oi < Math.min(oldIdx + range, oldLines.length); oi++) { + for (let ni = newIdx; ni < Math.min(newIdx + range, newLines.length); ni++) { + if (oi === oldIdx && ni === newIdx) continue; + const sim = calculateSimilarity(oldLines[oi], newLines[ni]); + if (sim > bestMatch.similarity) { + bestMatch = { oldIdx: oi, newIdx: ni, similarity: sim }; + } + } + } + + return bestMatch; + }; + + while (i < oldLines.length || j < newLines.length) { + if (i >= oldLines.length && j >= newLines.length) break; + + if (i < oldLines.length && j >= newLines.length) { + result.push({ + type: 'removed', + leftLine: oldLines[i], + rightLine: null, + leftNumber: leftNum++, + rightNumber: null + }); + i++; + continue; + } + + if (j < newLines.length && i >= oldLines.length) { + result.push({ + type: 'added', + leftLine: null, + rightLine: newLines[j], + leftNumber: null, + rightNumber: rightNum++ + }); + j++; + continue; + } + + const currentOld = oldLines[i]; + const currentNew = newLines[j]; + + if (currentOld === currentNew) { + result.push({ + type: 'unchanged', + leftLine: currentOld, + rightLine: currentNew, + leftNumber: leftNum++, + rightNumber: rightNum++ + }); + i++; + j++; + continue; + } + + const currentSimilarity = calculateSimilarity(currentOld, currentNew); + const lookahead = getLookaheadMatch(i, j, 10); + + const shouldPairCurrent = + currentSimilarity > 0.3 && + (lookahead.similarity - currentSimilarity < 0.2 || + (lookahead.oldIdx === i && lookahead.newIdx === j)); + + if (shouldPairCurrent) { + const charDiffs = generateCharDiffs(currentOld, currentNew); + result.push({ + type: 'modified', + leftLine: currentOld, + rightLine: currentNew, + leftNumber: leftNum++, + rightNumber: rightNum++, + charDiffs + }); + i++; + j++; + } else if (lookahead.oldIdx > i && lookahead.newIdx === j) { + result.push({ + type: 'removed', + leftLine: currentOld, + rightLine: null, + leftNumber: leftNum++, + rightNumber: null + }); + i++; + } else if (lookahead.newIdx > j && lookahead.oldIdx === i) { + result.push({ + type: 'added', + leftLine: null, + rightLine: currentNew, + leftNumber: null, + rightNumber: rightNum++ + }); + j++; + } else { + let oldHasBetterMatch = false; + let newHasBetterMatch = false; + + for (let k = j + 1; k < Math.min(j + 5, newLines.length); k++) { + if (calculateSimilarity(currentOld, newLines[k]) > currentSimilarity + 0.2) { + oldHasBetterMatch = true; + break; + } + } + + for (let k = i + 1; k < Math.min(i + 5, oldLines.length); k++) { + if (calculateSimilarity(oldLines[k], currentNew) > currentSimilarity + 0.2) { + newHasBetterMatch = true; + break; + } + } + + if (newHasBetterMatch && !oldHasBetterMatch) { + result.push({ + type: 'added', + leftLine: null, + rightLine: currentNew, + leftNumber: null, + rightNumber: rightNum++ + }); + j++; + } else if (oldHasBetterMatch && !newHasBetterMatch) { + result.push({ + type: 'removed', + leftLine: currentOld, + rightLine: null, + leftNumber: leftNum++, + rightNumber: null + }); + i++; + } else { + if (currentSimilarity > 0.2) { + const charDiffs = generateCharDiffs(currentOld, currentNew); + result.push({ + type: 'modified', + leftLine: currentOld, + rightLine: currentNew, + leftNumber: leftNum++, + rightNumber: rightNum++, + charDiffs + }); + i++; + j++; + } else { + result.push({ + type: 'removed', + leftLine: currentOld, + rightLine: null, + leftNumber: leftNum++, + rightNumber: null + }); + i++; + } + } + } + } + + return result; +} diff --git a/src/renderer/routes/automate/tools/session-comparison/utils/networkParser.ts b/src/renderer/routes/automate/tools/session-comparison/utils/networkParser.ts new file mode 100644 index 0000000..c3c39f0 --- /dev/null +++ b/src/renderer/routes/automate/tools/session-comparison/utils/networkParser.ts @@ -0,0 +1,61 @@ +import { NetworkComparison, NetworkEntry } from '../types'; + +export function parseHarLogs(harData: string | null): Record | null { + if (!harData) return null; + try { + const har = JSON.parse(harData); + if (!har?.log?.entries?.length) return null; + const groupedEntries: Record = {}; + har.log.entries.forEach((entry: any) => { + const key = `${entry.request.method}|||${entry.request.url}`; + if (!groupedEntries[key]) groupedEntries[key] = []; + groupedEntries[key].push(entry); + }); + return groupedEntries; + } catch (e) { + console.error('Error parsing HAR data:', e); + return null; + } +} + +export function formatNetworkEntry(key: string, entries: any[]): string { + const [method, url] = key.split('|||'); + const firstEntry = entries[0]; + const response = firstEntry.response; + const timings = firstEntry.timings || {}; + const statusCodes = entries.map((e: any) => e.response.status); + const uniqueStatuses = [...new Set(statusCodes)]; + const avgTime = entries.reduce((sum, e) => sum + (e.time || 0), 0) / entries.length; + const minTime = Math.min(...entries.map((e: any) => e.time || 0)); + const maxTime = Math.max(...entries.map((e: any) => e.time || 0)); + + return `Count: ${entries.length} | Status: ${uniqueStatuses.join(', ')} +Time: ${avgTime.toFixed(1)}ms (${minTime}-${maxTime}ms) +Size: ${response.bodySize || 0}b | Type: ${response.content?.mimeType || 'unknown'} +Timings: ${Object.entries(timings) + .filter(([_, v]) => v !== -1) + .map(([k, v]) => `${k}:${v}ms`) + .join(' ')}`; +} + +export function compareNetworkLogs(harA: string | null, harB: string | null): NetworkComparison { + const harLogsA = parseHarLogs(harA); + const harLogsB = parseHarLogs(harB); + if (!harLogsA && !harLogsB) return { matched: [], onlyInA: [], onlyInB: [] }; + + const matched: Array<{ key: string; dataA: any[]; dataB: any[] }> = []; + const onlyInA: Array<{ key: string; data: any[] }> = []; + const onlyInB: Array<{ key: string; data: any[] }> = []; + const keysA = new Set(Object.keys(harLogsA || {})); + const keysB = new Set(Object.keys(harLogsB || {})); + + keysA.forEach(key => { + if (keysB.has(key)) matched.push({ key, dataA: harLogsA![key], dataB: harLogsB![key] }); + else onlyInA.push({ key, data: harLogsA![key] }); + }); + keysB.forEach(key => { + if (!keysA.has(key)) onlyInB.push({ key, data: harLogsB![key] }); + }); + + return { matched, onlyInA, onlyInB }; +} diff --git a/src/renderer/routes/automate/tools/session-comparison/utils/sanitization.ts b/src/renderer/routes/automate/tools/session-comparison/utils/sanitization.ts new file mode 100644 index 0000000..b563c51 --- /dev/null +++ b/src/renderer/routes/automate/tools/session-comparison/utils/sanitization.ts @@ -0,0 +1,64 @@ +const BSTACK_OPTIONS_PATH = 'bstack:options'; +const ACCESSIBILITY_OPTIONS_PATH = 'accessibilityOptions'; +const AUTH_TOKEN_KEY = 'authToken'; +const REDACTED_VALUE = '***REDACTED***'; + +export function sanitizeCapabilities(session: any) { + if (!session) { + return session; + } + + const sanitizedSession = JSON.parse(JSON.stringify(session)); + + if (sanitizedSession[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + sanitizedSession[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = REDACTED_VALUE; + } + + const alwaysMatch = sanitizedSession.W3C_capabilities?.alwaysMatch; + if (alwaysMatch?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + alwaysMatch[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = REDACTED_VALUE; + } + + const firstMatch = sanitizedSession.W3C_capabilities?.firstMatch; + if (Array.isArray(firstMatch)) { + firstMatch.forEach((match: any) => { + if (match?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + match[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = REDACTED_VALUE; + } + }); + } + + return sanitizedSession; +} + +export function sanitizeTextLogs(textLogsResult: any) { + if (!textLogsResult || !textLogsResult.capabilities) { + return textLogsResult; + } + + const sanitized = JSON.parse(JSON.stringify(textLogsResult)); + + if (Array.isArray(sanitized.capabilities)) { + sanitized.capabilities.forEach((cap: any) => { + if (cap?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + cap[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = REDACTED_VALUE; + } + + const alwaysMatch = cap.W3C_capabilities?.alwaysMatch; + if (alwaysMatch?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + alwaysMatch[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = REDACTED_VALUE; + } + + const firstMatch = cap.W3C_capabilities?.firstMatch; + if (Array.isArray(firstMatch)) { + firstMatch.forEach((match: any) => { + if (match?.[BSTACK_OPTIONS_PATH]?.[ACCESSIBILITY_OPTIONS_PATH]?.[AUTH_TOKEN_KEY]) { + match[BSTACK_OPTIONS_PATH][ACCESSIBILITY_OPTIONS_PATH][AUTH_TOKEN_KEY] = REDACTED_VALUE; + } + }); + } + }); + } + + return sanitized; +} diff --git a/temp.html b/temp.html new file mode 100644 index 0000000..39d6ffe --- /dev/null +++ b/temp.html @@ -0,0 +1,120 @@ + + + + + Build Execution Report + + + + +
+ +

Build Execution Report

+ +

Build Information

+ + + + +
StatusPASSED
Triggered Bydemo_user@example.com
Triggered At2024-03-07T07:36:02.695Z
+ +

Execution Summary

+ + + + + + + +
Total Tests12
Passed8
Failed4
Running0
Queued0
Duration (sec)1200
+ +

Build Details

+ + + +
Build ID315d0cfdf7d77a42243ff87c0d6764cc8d3f9b09
Build NameSmoke Suite-1
+ +

Test Executions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameURLBrowserVersionTypeStatusDuration (sec)
Sample Test 02 | Bstackdemo add to carthttps://bstackdemo.comchrome102.0desktopPASSED300
Sample Test 02 | Bstackdemo add to carthttps://bstackdemo.comsafari16.5desktopPASSED300
+ +
+ + +