diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b7a6c2f7..e78be09c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,6 @@ on: - v* pull_request: - jobs: ci: runs-on: ${{ matrix.os }} @@ -26,7 +25,6 @@ jobs: - run: npm run build - run: npm run lint - run: npm run test - #code coverage server is failing, so disable it for now. (If you're reading this, re-enable please) #- run: npm run publish-coverage npm-release: #only run this task if a tag starting with 'v' was used to trigger this (i.e. a tagged release) diff --git a/.vscode/launch.json b/.vscode/launch.json index 981537e0..0ca8a376 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,7 +2,7 @@ "version": "0.2.0", "configurations": [ { - "type": "pwa-node", + "type": "node", "request": "launch", "name": "Run Roku Sample Project", "skipFiles": [ @@ -17,7 +17,7 @@ }, { "name": "Debug Tests", - "type": "pwa-node", + "type": "node", "request": "launch", "smartStep": false, "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", @@ -34,4 +34,4 @@ "internalConsoleOptions": "openOnSessionStart" } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 049d514f..19a6f424 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "files.trimTrailingWhitespace": false, + "files.trimTrailingWhitespace": true, "[markdown]": { "files.trimTrailingWhitespace": false }, diff --git a/package-lock.json b/package-lock.json index ab0922e6..0880a347 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "@rokucommunity/logger": "^0.3.3", "@types/request": "^2.48.8", "brighterscript": "^0.65.8", - "dateformat": "~4", + "dateformat": "^4.6.3", + "debounce": "^1.2.1", "eol": "^0.9.1", "eventemitter3": "^4.0.7", "fast-glob": "^3.2.11", @@ -23,7 +24,7 @@ "replace-in-file": "^6.3.2", "replace-last": "^1.2.6", "roku-deploy": "^3.10.3", - "semver": "^7.5.3", + "semver": "^7.5.4", "serialize-error": "^8.1.0", "smart-buffer": "^4.2.0", "source-map": "^0.7.4", @@ -36,6 +37,7 @@ "devDependencies": { "@types/chai": "^4.2.22", "@types/dateformat": "~3", + "@types/debounce": "^1.2.1", "@types/decompress": "^4.2.4", "@types/dedent": "^0.7.0", "@types/find-in-files": "^0.5.1", @@ -43,6 +45,7 @@ "@types/glob": "^7.2.0", "@types/mocha": "^9.0.0", "@types/node": "^16.11.6", + "@types/request": "^2.48.8", "@types/semver": "^7.3.9", "@types/sinon": "^10.0.6", "@types/vscode": "^1.61.0", @@ -58,6 +61,7 @@ "mocha": "^9.1.3", "nyc": "^15.1.0", "p-defer": "^4.0.0", + "portfinder": "^1.0.32", "rimraf": "^3.0.2", "rmfr": "^2.0.0", "rxjs": "^7.4.0", @@ -69,8 +73,9 @@ }, "node_modules/@babel/code-frame": { "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/highlight": "^7.14.5" }, @@ -80,16 +85,18 @@ }, "node_modules/@babel/compat-data": { "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", + "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.8.tgz", + "integrity": "sha512-3UG9dsxvYBMYwRv+gS41WKHno4K60/9GPy1CJaH6xy3Elq8CTtvtjT5R5jmNhXfCYLX2mTw+7/aq5ak/gOE0og==", "dev": true, - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.15.8", "@babel/generator": "^7.15.8", @@ -116,25 +123,28 @@ } }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/core/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==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/@babel/generator": { "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz", + "integrity": "sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==", "dev": true, - "license": "MIT", "dependencies": { "@babel/types": "^7.15.6", "jsesc": "^2.5.1", @@ -146,16 +156,18 @@ }, "node_modules/@babel/generator/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==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/@babel/helper-compilation-targets": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz", + "integrity": "sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ==", "dev": true, - "license": "MIT", "dependencies": { "@babel/compat-data": "^7.15.0", "@babel/helper-validator-option": "^7.14.5", @@ -170,17 +182,19 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-function-name": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-get-function-arity": "^7.15.4", "@babel/template": "^7.15.4", @@ -192,8 +206,9 @@ }, "node_modules/@babel/helper-get-function-arity": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/types": "^7.15.4" }, @@ -203,8 +218,9 @@ }, "node_modules/@babel/helper-hoist-variables": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz", + "integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/types": "^7.15.4" }, @@ -214,8 +230,9 @@ }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz", + "integrity": "sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/types": "^7.15.4" }, @@ -225,8 +242,9 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz", + "integrity": "sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/types": "^7.15.4" }, @@ -236,8 +254,9 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.8.tgz", + "integrity": "sha512-DfAfA6PfpG8t4S6npwzLvTUpp0sS7JrcuaMiy1Y5645laRJIp/LiLGIBbQKaXSInK8tiGNI7FL7L8UvB8gdUZg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.15.4", "@babel/helper-replace-supers": "^7.15.4", @@ -254,8 +273,9 @@ }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz", + "integrity": "sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw==", "dev": true, - "license": "MIT", "dependencies": { "@babel/types": "^7.15.4" }, @@ -265,8 +285,9 @@ }, "node_modules/@babel/helper-replace-supers": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz", + "integrity": "sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.15.4", "@babel/helper-optimise-call-expression": "^7.15.4", @@ -279,8 +300,9 @@ }, "node_modules/@babel/helper-simple-access": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz", + "integrity": "sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/types": "^7.15.4" }, @@ -290,8 +312,9 @@ }, "node_modules/@babel/helper-split-export-declaration": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", "dev": true, - "license": "MIT", "dependencies": { "@babel/types": "^7.15.4" }, @@ -301,24 +324,27 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", + "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.4.tgz", + "integrity": "sha512-V45u6dqEJ3w2rlryYYXf6i9rQ5YMNu4FLS6ngs8ikblhu2VdR1AqAd6aJjBzmf2Qzh6KOLqKHxEN9+TFbAkAVQ==", "dev": true, - "license": "MIT", "dependencies": { "@babel/template": "^7.15.4", "@babel/traverse": "^7.15.4", @@ -330,8 +356,9 @@ }, "node_modules/@babel/highlight": { "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.14.5", "chalk": "^2.0.0", @@ -343,8 +370,9 @@ }, "node_modules/@babel/highlight/node_modules/ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -354,8 +382,9 @@ }, "node_modules/@babel/highlight/node_modules/chalk": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -367,29 +396,33 @@ }, "node_modules/@babel/highlight/node_modules/color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "1.1.3" } }, "node_modules/@babel/highlight/node_modules/color-name": { "version": "1.1.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, "node_modules/@babel/highlight/node_modules/has-flag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/@babel/highlight/node_modules/supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -399,8 +432,9 @@ }, "node_modules/@babel/parser": { "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", "dev": true, - "license": "MIT", "bin": { "parser": "bin/babel-parser.js" }, @@ -410,8 +444,9 @@ }, "node_modules/@babel/template": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.14.5", "@babel/parser": "^7.15.4", @@ -423,8 +458,9 @@ }, "node_modules/@babel/traverse": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", + "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.14.5", "@babel/generator": "^7.15.4", @@ -442,16 +478,18 @@ }, "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==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/@babel/types": { "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.14.9", "to-fast-properties": "^2.0.0" @@ -462,16 +500,18 @@ }, "node_modules/@cspotcode/source-map-consumer": { "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">= 12" } }, "node_modules/@cspotcode/source-map-support": { "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", "dev": true, - "license": "MIT", "dependencies": { "@cspotcode/source-map-consumer": "0.8.0" }, @@ -481,8 +521,9 @@ }, "node_modules/@eslint/eslintrc": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.3.tgz", + "integrity": "sha512-DHI1wDPoKCBPoLZA3qDR91+3te/wDSc1YhKg3jR8NxKKRJq2hwHwcWv31cSwSYvIBrmbENoYMWcenW8uproQqg==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -500,8 +541,9 @@ }, "node_modules/@humanwhocodes/config-array": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.6.0.tgz", + "integrity": "sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^1.2.0", "debug": "^4.1.1", @@ -513,13 +555,15 @@ }, "node_modules/@humanwhocodes/object-schema": { "version": "1.2.0", - "dev": true, - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", + "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "dev": true }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, - "license": "ISC", "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -533,23 +577,26 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -560,14 +607,16 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "engines": { "node": ">= 8" } }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -624,7 +673,8 @@ }, "node_modules/@rokucommunity/bslib": { "version": "0.1.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@rokucommunity/bslib/-/bslib-0.1.1.tgz", + "integrity": "sha512-2ox6EUL+UTtccTbD4dbVjZK3QHa0PHCqpoKMF8lZz9ayzzEP3iVPF8KZR6hOi6bxsIcbGXVjqmtCVkpC4P9SrA==" }, "node_modules/@rokucommunity/logger": { "version": "0.3.3", @@ -639,24 +689,27 @@ }, "node_modules/@sinonjs/commons": { "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^1.7.0" } }, "node_modules/@sinonjs/samsam": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", + "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^1.6.0", "lodash.get": "^4.4.2", @@ -665,38 +718,45 @@ }, "node_modules/@sinonjs/text-encoding": { "version": "0.7.1", - "dev": true, - "license": "(Unlicense OR Apache-2.0)" + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true }, "node_modules/@tsconfig/node10": { "version": "1.0.8", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true }, "node_modules/@tsconfig/node12": { "version": "1.0.9", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true }, "node_modules/@tsconfig/node14": { "version": "1.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true }, "node_modules/@tsconfig/node16": { "version": "1.0.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true }, "node_modules/@types/caseless": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", + "dev": true }, "node_modules/@types/chai": { "version": "4.2.22", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.22.tgz", + "integrity": "sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==", + "dev": true }, "node_modules/@types/dateformat": { "version": "3.0.1", @@ -704,6 +764,12 @@ "integrity": "sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g==", "dev": true }, + "node_modules/@types/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==", + "dev": true + }, "node_modules/@types/decompress": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.4.tgz", @@ -715,26 +781,30 @@ }, "node_modules/@types/dedent": { "version": "0.7.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A==", + "dev": true }, "node_modules/@types/find-in-files": { "version": "0.5.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/find-in-files/-/find-in-files-0.5.1.tgz", + "integrity": "sha512-kUPtvVXZn99bBHx08jAJgrI1NKWspuoX6RgqQgfNlH2debcwcowUV41P6Kfg4VDaCAr5KNBW9qdjIyKRnXVuBA==", + "dev": true }, "node_modules/@types/fs-extra": { "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/glob": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", "dev": true, - "license": "MIT", "dependencies": { "@types/minimatch": "*", "@types/node": "*" @@ -742,27 +812,32 @@ }, "node_modules/@types/json-schema": { "version": "7.0.9", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true }, "node_modules/@types/minimatch": { "version": "3.0.5", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true }, "node_modules/@types/mocha": { "version": "9.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", + "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", + "dev": true }, "node_modules/@types/node": { "version": "16.11.6", + "dev": true, "license": "MIT" }, "node_modules/@types/request": { "version": "2.48.8", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", + "dev": true, "dependencies": { "@types/caseless": "*", "@types/node": "*", @@ -772,13 +847,15 @@ }, "node_modules/@types/semver": { "version": "7.3.9", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz", + "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==", + "dev": true }, "node_modules/@types/sinon": { "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.6.tgz", + "integrity": "sha512-6EF+wzMWvBNeGrfP3Nx60hhx+FfwSg1JJBLAAP/IdIUq0EYkqCYf70VT3PhuhPX9eLD+Dp+lNdpb/ZeHG8Yezg==", "dev": true, - "license": "MIT", "dependencies": { "@sinonjs/fake-timers": "^7.1.0" } @@ -786,17 +863,20 @@ "node_modules/@types/tough-cookie": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" + "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", + "dev": true }, "node_modules/@types/vscode": { "version": "1.61.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.61.0.tgz", + "integrity": "sha512-9k5Nwq45hkRwdfCFY+eKXeQQSbPoA114mF7U/4uJXRBJeGIO7MuJdhF1PnaDN+lllL9iKGQtd6FFXShBXMNaFg==", + "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.2.0.tgz", + "integrity": "sha512-qQwg7sqYkBF4CIQSyRQyqsYvP+g/J0To9ZPVNJpfxfekl5RmdvQnFFTVVwpRtaUDFNvjfe/34TgY/dpc3MgNTw==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/experimental-utils": "5.2.0", "@typescript-eslint/scope-manager": "5.2.0", @@ -826,16 +906,18 @@ }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/@typescript-eslint/experimental-utils": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.2.0.tgz", + "integrity": "sha512-fWyT3Agf7n7HuZZRpvUYdFYbPk3iDCq6fgu3ulia4c7yxmPnwVBovdSOX7RL+k8u6hLbrXcdAehlWUVpGh6IEw==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "@typescript-eslint/scope-manager": "5.2.0", @@ -857,8 +939,9 @@ }, "node_modules/@typescript-eslint/parser": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.2.0.tgz", + "integrity": "sha512-Uyy4TjJBlh3NuA8/4yIQptyJb95Qz5PX//6p8n7zG0QnN4o3NF9Je3JHbVU7fxf5ncSXTmnvMtd/LDQWDk0YqA==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/scope-manager": "5.2.0", "@typescript-eslint/types": "5.2.0", @@ -883,8 +966,9 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.2.0.tgz", + "integrity": "sha512-RW+wowZqPzQw8MUFltfKYZfKXqA2qgyi6oi/31J1zfXJRpOn6tCaZtd9b5u9ubnDG2n/EMvQLeZrsLNPpaUiFQ==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/types": "5.2.0", "@typescript-eslint/visitor-keys": "5.2.0" @@ -899,8 +983,9 @@ }, "node_modules/@typescript-eslint/types": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.2.0.tgz", + "integrity": "sha512-cTk6x08qqosps6sPyP2j7NxyFPlCNsJwSDasqPNjEQ8JMD5xxj2NHxcLin5AJQ8pAVwpQ8BMI3bTxR0zxmK9qQ==", "dev": true, - "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -911,8 +996,9 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.2.0.tgz", + "integrity": "sha512-RsdXq2XmVgKbm9nLsE3mjNUM7BTr/K4DYR9WfFVMUuozHWtH5gMpiNZmtrMG8GR385EOSQ3kC9HiEMJWimxd/g==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/types": "5.2.0", "@typescript-eslint/visitor-keys": "5.2.0", @@ -937,8 +1023,9 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.2.0.tgz", + "integrity": "sha512-Nk7HizaXWWCUBfLA/rPNKMzXzWS8Wg9qHMuGtT+v2/YpPij4nVXrVJc24N/r5WrrmqK31jCrZxeHqIgqRzs0Xg==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/types": "5.2.0", "eslint-visitor-keys": "^3.0.0" @@ -953,35 +1040,40 @@ }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz", + "integrity": "sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@ungap/promise-all-settled": { "version": "1.1.2", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true }, "node_modules/@xml-tools/parser": { "version": "1.0.11", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@xml-tools/parser/-/parser-1.0.11.tgz", + "integrity": "sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==", "dependencies": { "chevrotain": "7.1.1" } }, "node_modules/@xml-tools/parser/node_modules/chevrotain": { "version": "7.1.1", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-7.1.1.tgz", + "integrity": "sha512-wy3mC1x4ye+O+QkEinVJkPf5u2vsrDIYW9G7ZuwFl6v/Yu0LwUuT2POsb+NUWApebyxfkQq6+yDfRExbnI5rcw==", "dependencies": { "regexp-to-ast": "0.5.0" } }, "node_modules/acorn": { "version": "8.5.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", + "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", "dev": true, - "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -991,24 +1083,27 @@ }, "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.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.4.0" } }, "node_modules/aggregate-error": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, - "license": "MIT", "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -1019,7 +1114,8 @@ }, "node_modules/ajv": { "version": "6.12.6", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1033,22 +1129,25 @@ }, "node_modules/ansi-colors": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/ansi-regex": { "version": "5.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { "version": "4.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { "color-convert": "^2.0.1" }, @@ -1061,7 +1160,8 @@ }, "node_modules/anymatch": { "version": "3.1.2", - "license": "ISC", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1072,8 +1172,9 @@ }, "node_modules/append-transform": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", "dev": true, - "license": "MIT", "dependencies": { "default-require-extensions": "^3.0.0" }, @@ -1083,65 +1184,75 @@ }, "node_modules/append-type": { "version": "1.0.2", - "dev": true, - "license": "MIT-0" + "resolved": "https://registry.npmjs.org/append-type/-/append-type-1.0.2.tgz", + "integrity": "sha512-hac740vT/SAbrFBLgLIWZqVT5PUAcGTWS5UkDDhr+OCizZSw90WKw6sWAEgGaYd2viIblggypMXwpjzHXOvAQg==", + "dev": true }, "node_modules/archy": { "version": "1.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true }, "node_modules/arg": { "version": "4.1.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true }, "node_modules/argparse": { "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, - "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } }, "node_modules/array-flat-polyfill": { "version": "1.0.1", - "license": "CC0-1.0", + "resolved": "https://registry.npmjs.org/array-flat-polyfill/-/array-flat-polyfill-1.0.1.tgz", + "integrity": "sha512-hfJmKupmQN0lwi0xG6FQ5U8Rd97RnIERplymOv/qpq8AoNKPPAnxJadjFA23FNWm88wykh9HmpLJUUwUtNU/iw==", "engines": { "node": ">=6.0.0" } }, "node_modules/array-to-sentence": { "version": "1.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/array-to-sentence/-/array-to-sentence-1.1.0.tgz", + "integrity": "sha512-YkwkMmPA2+GSGvXj1s9NZ6cc2LBtR+uSeWTy2IGi5MR1Wag4DdrcjTxA/YV/Fw+qKlBeXomneZgThEbm/wvZbw==", + "dev": true }, "node_modules/array-union": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/asn1": { "version": "0.2.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", "dependencies": { "safer-buffer": "~2.1.0" } }, "node_modules/assert-plus": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "engines": { "node": ">=0.8" } }, "node_modules/assert-valid-glob-opts": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-valid-glob-opts/-/assert-valid-glob-opts-1.0.0.tgz", + "integrity": "sha512-/mttty5Xh7wE4o7ttKaUpBJl0l04xWe3y6muy1j27gyzSsnceK0AYU9owPtUoL9z8+9hnPxztmuhdFZ7jRoyWw==", "dev": true, - "license": "CC0-1.0", "dependencies": { "glob-option-error": "^1.0.0", "validate-glob-opts": "^1.0.0" @@ -1149,19 +1260,31 @@ }, "node_modules/assertion-error": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true, - "license": "MIT", "engines": { "node": "*" } }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, "node_modules/asynckit": { "version": "0.4.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/aws-sign2": { "version": "0.7.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "engines": { "node": "*" } @@ -1173,7 +1296,8 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -1196,14 +1320,16 @@ }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", - "license": "BSD-3-Clause", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "dependencies": { "tweetnacl": "^0.14.3" } }, "node_modules/binary-extensions": { "version": "2.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "engines": { "node": ">=8" } @@ -1220,11 +1346,13 @@ }, "node_modules/bluebird": { "version": "3.7.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "node_modules/brace-expansion": { "version": "1.1.11", - "license": "MIT", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1232,7 +1360,8 @@ }, "node_modules/braces": { "version": "3.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dependencies": { "fill-range": "^7.0.1" }, @@ -1284,7 +1413,8 @@ }, "node_modules/brighterscript/node_modules/ansi-styles": { "version": "3.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dependencies": { "color-convert": "^1.9.0" }, @@ -1294,7 +1424,8 @@ }, "node_modules/brighterscript/node_modules/chalk": { "version": "2.4.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -1306,7 +1437,8 @@ }, "node_modules/brighterscript/node_modules/cliui": { "version": "7.0.4", - "license": "ISC", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -1315,14 +1447,16 @@ }, "node_modules/brighterscript/node_modules/color-convert": { "version": "1.9.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dependencies": { "color-name": "1.1.3" } }, "node_modules/brighterscript/node_modules/color-name": { "version": "1.1.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/brighterscript/node_modules/fs-extra": { "version": "8.1.0", @@ -1339,14 +1473,16 @@ }, "node_modules/brighterscript/node_modules/has-flag": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "engines": { "node": ">=4" } }, "node_modules/brighterscript/node_modules/serialize-error": { "version": "7.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", "dependencies": { "type-fest": "^0.13.1" }, @@ -1359,7 +1495,8 @@ }, "node_modules/brighterscript/node_modules/supports-color": { "version": "5.5.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dependencies": { "has-flag": "^3.0.0" }, @@ -1369,7 +1506,8 @@ }, "node_modules/brighterscript/node_modules/type-fest": { "version": "0.13.1", - "license": "(MIT OR CC0-1.0)", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "engines": { "node": ">=10" }, @@ -1379,7 +1517,8 @@ }, "node_modules/brighterscript/node_modules/vscode-languageserver": { "version": "7.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", + "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", "dependencies": { "vscode-languageserver-protocol": "3.16.0" }, @@ -1389,7 +1528,8 @@ }, "node_modules/brighterscript/node_modules/wrap-ansi": { "version": "7.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -1404,7 +1544,8 @@ }, "node_modules/brighterscript/node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { "color-convert": "^2.0.1" }, @@ -1417,7 +1558,8 @@ }, "node_modules/brighterscript/node_modules/wrap-ansi/node_modules/color-convert": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { "color-name": "~1.1.4" }, @@ -1427,18 +1569,21 @@ }, "node_modules/brighterscript/node_modules/wrap-ansi/node_modules/color-name": { "version": "1.1.4", - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/brighterscript/node_modules/y18n": { "version": "5.0.8", - "license": "ISC", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "engines": { "node": ">=10" } }, "node_modules/brighterscript/node_modules/yargs": { "version": "16.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -1454,7 +1599,8 @@ }, "node_modules/brighterscript/node_modules/yargs-parser": { "version": "20.2.9", - "license": "ISC", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "engines": { "node": ">=10" } @@ -1469,13 +1615,15 @@ }, "node_modules/browser-stdout": { "version": "1.3.1", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true }, "node_modules/browserslist": { "version": "4.17.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.5.tgz", + "integrity": "sha512-I3ekeB92mmpctWBoLXe0d5wPS2cBuRvvW0JyyJHMrk9/HmP2ZjrTboNAZ8iuGqaEIlKguljbQY32OkOJIRrgoA==", "dev": true, - "license": "MIT", "dependencies": { "caniuse-lite": "^1.0.30001271", "electron-to-chromium": "^1.3.878", @@ -1551,13 +1699,15 @@ }, "node_modules/buffer-from": { "version": "1.1.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true }, "node_modules/caching-transform": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", "dev": true, - "license": "MIT", "dependencies": { "hasha": "^5.0.0", "make-dir": "^3.0.0", @@ -1570,16 +1720,18 @@ }, "node_modules/callsites": { "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" } }, "node_modules/camelcase": { "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -1606,12 +1758,14 @@ }, "node_modules/caseless": { "version": "0.12.0", - "license": "Apache-2.0" + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, "node_modules/chai": { "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", "dev": true, - "license": "MIT", "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.2", @@ -1626,7 +1780,8 @@ }, "node_modules/chalk": { "version": "4.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1640,15 +1795,17 @@ }, "node_modules/check-error": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", "dev": true, - "license": "MIT", "engines": { "node": "*" } }, "node_modules/chevrotain": { "version": "7.1.2", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-7.1.2.tgz", + "integrity": "sha512-9bQsXVQ7UAvzMs7iUBBJ9Yv//exOy7bIR3PByOEk4M64vIE/LsiOiX7VIkMF/vEMlrSStwsaE884Bp9CpjtC5g==", "dependencies": { "regexp-to-ast": "0.5.0" } @@ -1681,22 +1838,26 @@ }, "node_modules/clean-stack": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/clear": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/clear/-/clear-0.1.0.tgz", + "integrity": "sha512-qMjRnoL+JDPJHeLePZJuao6+8orzHMGP04A8CdwCNsKhRbOnKRjefxONR7bwILT3MHecxKBjHkKL/tkZ8r4Uzw==", "engines": { "node": "*" } }, "node_modules/cliui": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dev": true, - "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -1705,7 +1866,8 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { "color-name": "~1.1.4" }, @@ -1715,11 +1877,13 @@ }, "node_modules/color-name": { "version": "1.1.4", - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/combined-stream": { "version": "1.0.8", - "license": "MIT", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -1735,24 +1899,28 @@ }, "node_modules/commondir": { "version": "1.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true }, "node_modules/concat-map": { "version": "0.0.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/convert-source-map": { "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "~5.1.1" } }, "node_modules/core-util-is": { "version": "1.0.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "node_modules/coveralls-next": { "version": "4.2.0", @@ -1803,17 +1971,20 @@ }, "node_modules/create-require": { "version": "1.1.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true }, "node_modules/cross-platform-clear-console": { "version": "2.3.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/cross-platform-clear-console/-/cross-platform-clear-console-2.3.0.tgz", + "integrity": "sha512-To+sJ6plHHC6k5DfdvSVn6F1GRGJh/R6p76bCpLbyMyHEmbqFyuMAeGwDcz/nGDWH3HUcjFTTX9iUSCzCg9Eiw==" }, "node_modules/cross-spawn": { "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, - "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1825,7 +1996,8 @@ }, "node_modules/dashdash": { "version": "1.14.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "dependencies": { "assert-plus": "^1.0.0" }, @@ -1846,9 +2018,15 @@ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, "node_modules/debounce-promise": { "version": "3.1.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz", + "integrity": "sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg==" }, "node_modules/debug": { "version": "4.3.3", @@ -1869,8 +2047,9 @@ }, "node_modules/decamelize": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2021,13 +2200,15 @@ }, "node_modules/dedent": { "version": "0.7.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true }, "node_modules/deep-eql": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", "dev": true, - "license": "MIT", "dependencies": { "type-detect": "^4.0.0" }, @@ -2037,13 +2218,15 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "dev": true, - "license": "MIT" + "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 }, "node_modules/default-require-extensions": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", "dev": true, - "license": "MIT", "dependencies": { "strip-bom": "^4.0.0" }, @@ -2053,31 +2236,35 @@ }, "node_modules/default-require-extensions/node_modules/strip-bom": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/delayed-stream": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "engines": { "node": ">=0.4.0" } }, "node_modules/diff": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, "node_modules/dir-glob": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, - "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -2087,8 +2274,9 @@ }, "node_modules/doctrine": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, - "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -2098,7 +2286,8 @@ }, "node_modules/ecc-jsbn": { "version": "0.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -2106,12 +2295,14 @@ }, "node_modules/electron-to-chromium": { "version": "1.3.880", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.880.tgz", + "integrity": "sha512-iwIP/6WoeSimzUKJIQtjtpVDsK8Ir8qQCMXsUBwg+rxJR2Uh3wTNSbxoYRfs+3UWx/9MAnPIxVZCyWkm8MT0uw==", + "dev": true }, "node_modules/emoji-regex": { "version": "8.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/end-of-stream": { "version": "1.4.4", @@ -2124,8 +2315,9 @@ }, "node_modules/enquirer": { "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", "dev": true, - "license": "MIT", "dependencies": { "ansi-colors": "^4.1.1" }, @@ -2135,31 +2327,36 @@ }, "node_modules/eol": { "version": "0.9.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==" }, "node_modules/es6-error": { "version": "4.1.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true }, "node_modules/escalade": { "version": "3.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "engines": { "node": ">=6" } }, "node_modules/escape-string-regexp": { "version": "1.0.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "engines": { "node": ">=0.8.0" } }, "node_modules/eslint": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.1.0.tgz", + "integrity": "sha512-JZvNneArGSUsluHWJ8g8MMs3CfIEzwaLx9KyH4tZ2i+R2/rPWzL8c0zg3rHdwYVpN/1sB9gqnjHwz9HoeJpGHw==", "dev": true, - "license": "MIT", "dependencies": { "@eslint/eslintrc": "^1.0.3", "@humanwhocodes/config-array": "^0.6.0", @@ -2212,16 +2409,18 @@ }, "node_modules/eslint-plugin-no-only-tests": { "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-2.6.0.tgz", + "integrity": "sha512-T9SmE/g6UV1uZo1oHAqOvL86XWl7Pl2EpRpnLI8g/bkJu+h7XBCB+1LnubRZ2CUQXj805vh4/CYZdnqtVaEo2Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=4.0.0" } }, "node_modules/eslint-scope": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -2232,8 +2431,9 @@ }, "node_modules/eslint-utils": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", "dev": true, - "license": "MIT", "dependencies": { "eslint-visitor-keys": "^2.0.0" }, @@ -2249,21 +2449,24 @@ }, "node_modules/eslint-visitor-keys": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=10" } }, "node_modules/eslint/node_modules/argparse": { "version": "2.0.1", - "dev": true, - "license": "Python-2.0" + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -2273,8 +2476,9 @@ }, "node_modules/eslint/node_modules/eslint-scope": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-6.0.0.tgz", + "integrity": "sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -2285,24 +2489,27 @@ }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz", + "integrity": "sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "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/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" }, @@ -2312,8 +2519,9 @@ }, "node_modules/eslint/node_modules/js-yaml": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -2323,8 +2531,9 @@ }, "node_modules/espree": { "version": "9.0.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.0.0.tgz", + "integrity": "sha512-r5EQJcYZ2oaGbeR0jR0fFVijGOcwai07/690YRXLINuhmVeRY4UKSAsQPe/0BNuDgwP7Ophoc1PRsr2E3tkbdQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.5.0", "acorn-jsx": "^5.3.1", @@ -2336,16 +2545,18 @@ }, "node_modules/espree/node_modules/eslint-visitor-keys": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz", + "integrity": "sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/esprima": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, - "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -2356,8 +2567,9 @@ }, "node_modules/esquery": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -2367,16 +2579,18 @@ }, "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", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -2386,46 +2600,53 @@ }, "node_modules/esrecurse/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/estraverse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/esutils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/eventemitter3": { "version": "4.0.7", - "license": "MIT" + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, "node_modules/extend": { "version": "3.0.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "node_modules/extsprintf": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "engines": [ "node >=0.6.0" - ], - "license": "MIT" + ] }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.2.12", @@ -2444,16 +2665,19 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, "node_modules/fastq": { "version": "1.13.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", "dependencies": { "reusify": "^1.0.4" } @@ -2469,8 +2693,9 @@ }, "node_modules/file-entry-cache": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, - "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, @@ -2489,14 +2714,16 @@ }, "node_modules/file-url": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/file-url/-/file-url-3.0.0.tgz", + "integrity": "sha512-g872QGsHexznxkIAdK8UiZRe7SkE6kvylShU4Nsj8NvfvZag7S0QuQ4IgvPDkk75HxgjIVDwycFTDAgIiO4nDA==", "engines": { "node": ">=8" } }, "node_modules/fill-range": { "version": "7.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2506,14 +2733,17 @@ }, "node_modules/find": { "version": "0.1.7", + "resolved": "https://registry.npmjs.org/find/-/find-0.1.7.tgz", + "integrity": "sha512-jPrupTOe/pO//3a9Ty2o4NqQCp0L46UG+swUnfFtdmtQVN8pEltKpAqR7Nuf6vWn0GBXx5w+R1MyZzqwjEIqdA==", "dependencies": { "traverse-chain": "~0.1.0" } }, "node_modules/find-cache-dir": { "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "dev": true, - "license": "MIT", "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", @@ -2528,8 +2758,9 @@ }, "node_modules/find-cache-dir/node_modules/pkg-dir": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, - "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, @@ -2539,7 +2770,8 @@ }, "node_modules/find-in-files": { "version": "0.5.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/find-in-files/-/find-in-files-0.5.0.tgz", + "integrity": "sha512-VraTc6HdtdSHmAp0yJpAy20yPttGKzyBWc7b7FPnnsX9TOgmKx0g9xajizpF/iuu4IvNK4TP0SpyBT9zAlwG+g==", "dependencies": { "find": "^0.1.5", "q": "^1.0.1" @@ -2547,8 +2779,9 @@ }, "node_modules/find-up": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, - "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -2559,16 +2792,18 @@ }, "node_modules/flat": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, - "license": "BSD-3-Clause", "bin": { "flat": "cli.js" } }, "node_modules/flat-cache": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "dev": true, - "license": "MIT", "dependencies": { "flatted": "^3.1.0", "rimraf": "^3.0.2" @@ -2584,8 +2819,9 @@ }, "node_modules/foreground-child": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", "dev": true, - "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^3.0.2" @@ -2596,7 +2832,8 @@ }, "node_modules/forever-agent": { "version": "0.6.1", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "engines": { "node": "*" } @@ -2605,6 +2842,7 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -2616,6 +2854,8 @@ }, "node_modules/fromentries": { "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", "dev": true, "funding": [ { @@ -2630,8 +2870,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/fs-constants": { "version": "1.0.0", @@ -2641,7 +2880,8 @@ }, "node_modules/fs-extra": { "version": "10.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -2653,7 +2893,8 @@ }, "node_modules/fs-extra/node_modules/jsonfile": { "version": "6.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dependencies": { "universalify": "^2.0.0" }, @@ -2663,18 +2904,22 @@ }, "node_modules/fs-extra/node_modules/universalify": { "version": "2.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "engines": { "node": ">= 10.0.0" } }, "node_modules/fs.realpath": { "version": "1.0.0", - "license": "ISC" + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -2685,44 +2930,50 @@ }, "node_modules/functional-red-black-tree": { "version": "1.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true }, "node_modules/gensync": { "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/get-caller-file": { "version": "2.0.5", - "license": "ISC", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-func-name": { - "version": "2.0.0", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, - "license": "MIT", "engines": { "node": "*" } }, "node_modules/get-package-type": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.0.0" } }, "node_modules/get-port": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -2745,14 +2996,16 @@ }, "node_modules/getpass": { "version": "0.1.7", - "license": "MIT", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "dependencies": { "assert-plus": "^1.0.0" } }, "node_modules/glob": { "version": "7.2.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2770,12 +3023,14 @@ }, "node_modules/glob-option-error": { "version": "1.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/glob-option-error/-/glob-option-error-1.0.0.tgz", + "integrity": "sha512-AD7lbWbwF2Ii9gBQsQIOEzwuqP/jsnyvK27/3JDq1kn/JyfDtYI6AWz3ZQwcPuQdHSBcFh+A2yT/SEep27LOGg==", + "dev": true }, "node_modules/glob-parent": { "version": "5.1.2", - "license": "ISC", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dependencies": { "is-glob": "^4.0.1" }, @@ -2785,8 +3040,9 @@ }, "node_modules/globals": { "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", "dev": true, - "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -2799,8 +3055,9 @@ }, "node_modules/globby": { "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", "dev": true, - "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -2818,34 +3075,40 @@ }, "node_modules/globby/node_modules/ignore": { "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/graceful-fs": { "version": "4.2.8", - "license": "ISC" + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" }, "node_modules/growl": { "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true, - "license": "MIT", "engines": { "node": ">=4.x" } }, "node_modules/har-schema": { "version": "2.0.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", "engines": { "node": ">=4" } }, "node_modules/har-validator": { "version": "5.1.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", "dependencies": { "ajv": "^6.12.3", "har-schema": "^2.0.0" @@ -2856,15 +3119,17 @@ }, "node_modules/has-flag": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { "node": ">=8" } }, "node_modules/hasha": { "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", "dev": true, - "license": "MIT", "dependencies": { "is-stream": "^2.0.0", "type-fest": "^0.8.0" @@ -2878,16 +3143,18 @@ }, "node_modules/hasha/node_modules/type-fest": { "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true, - "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=8" } }, "node_modules/he": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, - "license": "MIT", "bin": { "he": "bin/he" } @@ -2919,20 +3186,23 @@ }, "node_modules/ignore": { "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/immediate": { "version": "3.0.6", - "license": "MIT" + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" }, "node_modules/import-fresh": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, - "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2946,31 +3216,35 @@ }, "node_modules/imurmurhash": { "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" } }, "node_modules/indent-string": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/indexed-filter": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/indexed-filter/-/indexed-filter-1.0.3.tgz", + "integrity": "sha512-oBIzs6EARNMzrLgVg20fK52H19WcRHBiukiiEkw9rnnI//8rinEBMLrYdwEfJ9d4K7bjV1L6nSGft6H/qzHNgQ==", "dev": true, - "license": "ISC", "dependencies": { "append-type": "^1.0.1" } }, "node_modules/inflight": { "version": "1.0.6", - "license": "ISC", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2978,19 +3252,22 @@ }, "node_modules/inherits": { "version": "2.0.4", - "license": "ISC" + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/inspect-with-kind": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz", + "integrity": "sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==", "dev": true, - "license": "ISC", "dependencies": { "kind-of": "^6.0.2" } }, "node_modules/is-binary-path": { "version": "2.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -3000,21 +3277,24 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "engines": { "node": ">=0.10.0" } }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "engines": { "node": ">=8" } }, "node_modules/is-glob": { "version": "4.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dependencies": { "is-extglob": "^2.1.1" }, @@ -3030,23 +3310,26 @@ }, "node_modules/is-number": { "version": "7.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "engines": { "node": ">=0.12.0" } }, "node_modules/is-plain-obj": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-stream": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -3056,12 +3339,14 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "node_modules/is-unicode-supported": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -3071,37 +3356,43 @@ }, "node_modules/is-windows": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/isarray": { "version": "1.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/isexe": { "version": "2.0.0", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/isstream": { "version": "0.1.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-hook": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "append-transform": "^2.0.0" }, @@ -3111,8 +3402,9 @@ }, "node_modules/istanbul-lib-instrument": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.7.5", "@istanbuljs/schema": "^0.1.2", @@ -3124,17 +3416,19 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/istanbul-lib-processinfo": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", "dev": true, - "license": "ISC", "dependencies": { "archy": "^1.0.0", "cross-spawn": "^7.0.0", @@ -3150,8 +3444,9 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^3.0.0", @@ -3163,8 +3458,9 @@ }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", @@ -3176,16 +3472,18 @@ }, "node_modules/istanbul-lib-source-maps/node_modules/source-map": { "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" } }, "node_modules/istanbul-reports": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.5.tgz", + "integrity": "sha512-5+19PlhnGabNWB7kOFnuxT8H3T/iIyQzIbQMxXsURmmvKg86P2sbkrGOT77VnHw0Qr0gc2XzRaRfMZYYbSQCJQ==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -3196,13 +3494,15 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "node_modules/js-yaml": { "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -3213,12 +3513,14 @@ }, "node_modules/jsbn": { "version": "0.1.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, "node_modules/jsesc": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true, - "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -3228,20 +3530,24 @@ }, "node_modules/json-schema": { "version": "0.4.0", - "license": "(AFL-2.1 OR BSD-3-Clause)" + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "dev": true, - "license": "MIT" + "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 }, "node_modules/json-stringify-safe": { "version": "5.0.1", - "license": "ISC" + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, "node_modules/json5": { "version": "2.2.3", @@ -3257,11 +3563,13 @@ }, "node_modules/jsonc-parser": { "version": "2.3.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==" }, "node_modules/jsonfile": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -3279,13 +3587,15 @@ }, "node_modules/just-extend": { "version": "4.2.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true }, "node_modules/kind-of": { "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" } @@ -3299,8 +3609,9 @@ }, "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" @@ -3311,15 +3622,17 @@ }, "node_modules/lie": { "version": "3.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "dependencies": { "immediate": "~3.0.5" } }, "node_modules/locate-path": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, - "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -3327,20 +3640,29 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.flattendeep": { "version": "4.4.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true }, "node_modules/lodash.get": { "version": "4.4.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true }, "node_modules/lodash.merge": { "version": "4.6.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, "node_modules/log-driver": { "version": "1.2.7", @@ -3351,8 +3673,9 @@ }, "node_modules/log-symbols": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, - "license": "MIT", "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -3366,14 +3689,16 @@ }, "node_modules/long": { "version": "3.2.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "integrity": "sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg==", "engines": { "node": ">=0.6" } }, "node_modules/lru-cache": { "version": "6.0.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dependencies": { "yallist": "^4.0.0" }, @@ -3391,8 +3716,9 @@ }, "node_modules/make-dir": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, - "license": "MIT", "dependencies": { "semver": "^6.0.0" }, @@ -3404,28 +3730,32 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/make-error": { "version": "1.3.6", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true }, "node_modules/merge2": { "version": "1.4.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "engines": { "node": ">= 8" } }, "node_modules/micromatch": { "version": "4.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "dependencies": { "braces": "^3.0.1", "picomatch": "^2.2.3" @@ -3474,7 +3804,8 @@ }, "node_modules/mkdirp": { "version": "1.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "bin": { "mkdirp": "bin/cmd.js" }, @@ -3527,13 +3858,15 @@ }, "node_modules/mocha/node_modules/argparse": { "version": "2.0.1", - "dev": true, - "license": "Python-2.0" + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/mocha/node_modules/cliui": { "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, - "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -3542,8 +3875,9 @@ }, "node_modules/mocha/node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -3553,8 +3887,9 @@ }, "node_modules/mocha/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" @@ -3568,8 +3903,9 @@ }, "node_modules/mocha/node_modules/js-yaml": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -3579,8 +3915,9 @@ }, "node_modules/mocha/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" }, @@ -3605,13 +3942,15 @@ }, "node_modules/mocha/node_modules/ms": { "version": "2.1.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, "node_modules/mocha/node_modules/p-limit": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -3624,8 +3963,9 @@ }, "node_modules/mocha/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" }, @@ -3638,8 +3978,9 @@ }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -3652,8 +3993,9 @@ }, "node_modules/mocha/node_modules/wrap-ansi": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -3668,16 +4010,18 @@ }, "node_modules/mocha/node_modules/y18n": { "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, - "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/mocha/node_modules/yargs": { "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, - "license": "MIT", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -3693,8 +4037,9 @@ }, "node_modules/mocha/node_modules/yargs-parser": { "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", "dev": true, - "license": "ISC", "engines": { "node": ">=10" } @@ -3709,8 +4054,9 @@ }, "node_modules/ms": { "version": "2.1.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/nanoid": { "version": "3.3.1", @@ -3726,20 +4072,23 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, "node_modules/natural-orderby": { "version": "2.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-2.0.3.tgz", + "integrity": "sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q==", "engines": { "node": "*" } }, "node_modules/nise": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", + "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^1.7.0", "@sinonjs/fake-timers": "^7.0.4", @@ -3750,8 +4099,9 @@ }, "node_modules/node-preload": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", "dev": true, - "license": "MIT", "dependencies": { "process-on-spawn": "^1.0.0" }, @@ -3761,20 +4111,23 @@ }, "node_modules/node-releases": { "version": "2.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", + "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", + "dev": true }, "node_modules/normalize-path": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "engines": { "node": ">=0.10.0" } }, "node_modules/nyc": { "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", "dev": true, - "license": "ISC", "dependencies": { "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", @@ -3813,15 +4166,17 @@ }, "node_modules/nyc/node_modules/resolve-from": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/oauth-sign": { "version": "0.9.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "engines": { "node": "*" } @@ -3837,15 +4192,17 @@ }, "node_modules/once": { "version": "1.4.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dependencies": { "wrappy": "1" } }, "node_modules/optionator": { "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", "dev": true, - "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -3860,8 +4217,9 @@ }, "node_modules/p-defer": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-4.0.0.tgz", + "integrity": "sha512-Vb3QRvQ0Y5XnF40ZUWW7JfLogicVh/EnA5gBIvKDJoYpeI82+1E3AlB9yOcKFS0AhHrWVnAQO39fbR0G99IVEQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -3871,8 +4229,9 @@ }, "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==", "dev": true, - "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -3885,8 +4244,9 @@ }, "node_modules/p-locate": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, - "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -3896,8 +4256,9 @@ }, "node_modules/p-map": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", "dev": true, - "license": "MIT", "dependencies": { "aggregate-error": "^3.0.0" }, @@ -3907,14 +4268,16 @@ }, "node_modules/p-reflect": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/p-reflect/-/p-reflect-1.0.0.tgz", + "integrity": "sha512-rlngKS+EX3nvI7xIzA0xKNVEAguWdIqAZVbn02z1m73ehXBdX66aTdD0bCvIu0cDwbU3TK9w3RYrppKpO3EnKQ==", "engines": { "node": ">=4" } }, "node_modules/p-settle": { "version": "2.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/p-settle/-/p-settle-2.1.0.tgz", + "integrity": "sha512-NHFIUYc+fQTFRrzzAugq0l1drwi57PB522smetcY8C/EoTYs6cU/fC6TJj0N3rq5NhhJJbhf0VGWziL3jZDnjA==", "dependencies": { "p-limit": "^1.2.0", "p-reflect": "^1.0.0" @@ -3925,7 +4288,8 @@ }, "node_modules/p-settle/node_modules/p-limit": { "version": "1.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dependencies": { "p-try": "^1.0.0" }, @@ -3935,23 +4299,26 @@ }, "node_modules/p-settle/node_modules/p-try": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", "engines": { "node": ">=4" } }, "node_modules/p-try": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/package-hash": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", "dev": true, - "license": "ISC", "dependencies": { "graceful-fs": "^4.1.15", "hasha": "^5.0.0", @@ -3964,12 +4331,14 @@ }, "node_modules/pako": { "version": "1.0.11", - "license": "(MIT AND Zlib)" + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "node_modules/parent-module": { "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" }, @@ -3979,59 +4348,67 @@ }, "node_modules/parse-ms": { "version": "2.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", "engines": { "node": ">=6" } }, "node_modules/path-exists": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-is-absolute": { "version": "1.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "engines": { "node": ">=0.10.0" } }, "node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-to-regexp": { "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", "dev": true, - "license": "MIT", "dependencies": { "isarray": "0.0.1" } }, "node_modules/path-to-regexp/node_modules/isarray": { "version": "0.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true }, "node_modules/path-type": { "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" } }, "node_modules/pathval": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, - "license": "MIT", "engines": { "node": "*" } @@ -4044,16 +4421,19 @@ }, "node_modules/performance-now": { "version": "2.1.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "node_modules/picocolors": { "version": "1.0.0", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true }, "node_modules/picomatch": { "version": "2.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", "engines": { "node": ">=8.6" }, @@ -4091,6 +4471,41 @@ "node": ">=0.10.0" } }, + "node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "dev": true, + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/portfinder/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/postman-request": { "version": "2.88.1-postman.32", "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.32.tgz", @@ -4160,20 +4575,23 @@ }, "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/process-nextick-args": { "version": "2.0.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/process-on-spawn": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", "dev": true, - "license": "MIT", "dependencies": { "fromentries": "^1.2.0" }, @@ -4183,26 +4601,30 @@ }, "node_modules/progress": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.4.0" } }, "node_modules/psl": { "version": "1.8.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, "node_modules/punycode": { "version": "2.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "engines": { "node": ">=6" } }, "node_modules/q": { "version": "1.5.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", "engines": { "node": ">=0.6.0", "teleport": ">=0.2.0" @@ -4223,6 +4645,8 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "funding": [ { "type": "github", @@ -4236,20 +4660,21 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/randombytes": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } }, "node_modules/readable-stream": { "version": "2.3.7", - "license": "MIT", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4278,12 +4703,14 @@ }, "node_modules/regexp-to-ast": { "version": "0.5.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==" }, "node_modules/regexpp": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -4293,8 +4720,9 @@ }, "node_modules/release-zalgo": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", "dev": true, - "license": "ISC", "dependencies": { "es6-error": "^4.0.1" }, @@ -4304,7 +4732,8 @@ }, "node_modules/replace-in-file": { "version": "6.3.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.3.2.tgz", + "integrity": "sha512-Dbt5pXKvFVPL3WAaEB3ZX+95yP0CeAtIPJDwYzHbPP5EAHn+0UoegH/Wg3HKflU9dYBH8UnBC2NvY3P+9EZtTg==", "dependencies": { "chalk": "^4.1.2", "glob": "^7.2.0", @@ -4319,7 +4748,8 @@ }, "node_modules/replace-in-file/node_modules/cliui": { "version": "7.0.4", - "license": "ISC", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -4328,7 +4758,8 @@ }, "node_modules/replace-in-file/node_modules/wrap-ansi": { "version": "7.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -4343,14 +4774,16 @@ }, "node_modules/replace-in-file/node_modules/y18n": { "version": "5.0.8", - "license": "ISC", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "engines": { "node": ">=10" } }, "node_modules/replace-in-file/node_modules/yargs": { "version": "17.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz", + "integrity": "sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -4366,29 +4799,33 @@ }, "node_modules/replace-in-file/node_modules/yargs-parser": { "version": "20.2.9", - "license": "ISC", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "engines": { "node": ">=10" } }, "node_modules/replace-last": { "version": "1.2.6", - "license": "ISC", + "resolved": "https://registry.npmjs.org/replace-last/-/replace-last-1.2.6.tgz", + "integrity": "sha512-Cj+MK38VtNu1S5J73mEZY3ciQb9dJajNq1Q8inP4dn/MhJMjHwoAF3Z3FjspwAEV9pfABl565MQucmrjOkty4g==", "engines": { "node": ">= 4.0.0" } }, "node_modules/require-directory": { "version": "2.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "engines": { "node": ">=0.10.0" } }, "node_modules/require-main-filename": { "version": "2.0.0", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true }, "node_modules/require-relative": { "version": "0.8.7", @@ -4402,15 +4839,17 @@ }, "node_modules/resolve-from": { "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" } }, "node_modules/reusify": { "version": "1.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -4418,8 +4857,9 @@ }, "node_modules/rimraf": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, - "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -4432,8 +4872,9 @@ }, "node_modules/rmfr": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rmfr/-/rmfr-2.0.0.tgz", + "integrity": "sha512-nQptLCZeyyJfgbpf2x97k5YE8vzDn7bhwx9NlvODdhgbU0mL1ruh71X0HYdRaOEvWC7Cr+SfV0p5p+Ib5yOl7A==", "dev": true, - "license": "ISC", "dependencies": { "assert-valid-glob-opts": "^1.0.0", "glob": "^7.1.2", @@ -4444,8 +4885,9 @@ }, "node_modules/rmfr/node_modules/rimraf": { "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dev": true, - "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -4479,7 +4921,8 @@ }, "node_modules/roku-deploy/node_modules/ansi-styles": { "version": "3.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dependencies": { "color-convert": "^1.9.0" }, @@ -4489,7 +4932,8 @@ }, "node_modules/roku-deploy/node_modules/chalk": { "version": "2.4.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -4501,25 +4945,29 @@ }, "node_modules/roku-deploy/node_modules/color-convert": { "version": "1.9.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dependencies": { "color-name": "1.1.3" } }, "node_modules/roku-deploy/node_modules/color-name": { "version": "1.1.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/roku-deploy/node_modules/dateformat": { "version": "3.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", "engines": { "node": "*" } }, "node_modules/roku-deploy/node_modules/fs-extra": { "version": "7.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -4531,14 +4979,16 @@ }, "node_modules/roku-deploy/node_modules/has-flag": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "engines": { "node": ">=4" } }, "node_modules/roku-deploy/node_modules/supports-color": { "version": "5.5.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dependencies": { "has-flag": "^3.0.0" }, @@ -4548,6 +4998,8 @@ }, "node_modules/run-parallel": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "funding": [ { "type": "github", @@ -4562,35 +5014,39 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } }, "node_modules/rxjs": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz", + "integrity": "sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==", "dev": true, - "license": "Apache-2.0", "dependencies": { "tslib": "~2.1.0" } }, "node_modules/rxjs/node_modules/tslib": { "version": "2.1.0", - "dev": true, - "license": "0BSD" + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "dev": true }, "node_modules/safe-buffer": { "version": "5.1.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/safe-json-stringify": { "version": "1.2.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==" }, "node_modules/safer-buffer": { "version": "2.1.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sax": { "version": "1.2.4", @@ -4611,9 +5067,9 @@ } }, "node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -4626,7 +5082,8 @@ }, "node_modules/serialize-error": { "version": "8.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", "dependencies": { "type-fest": "^0.20.2" }, @@ -4639,16 +5096,18 @@ }, "node_modules/serialize-javascript": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } }, "node_modules/set-blocking": { "version": "2.0.0", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true }, "node_modules/setimmediate": { "version": "1.0.5", @@ -4657,8 +5116,9 @@ }, "node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -4668,21 +5128,24 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/signal-exit": { "version": "3.0.5", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", + "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", + "dev": true }, "node_modules/sinon": { "version": "11.1.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz", + "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^1.8.3", "@sinonjs/fake-timers": "^7.1.2", @@ -4698,15 +5161,17 @@ }, "node_modules/slash": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/smart-buffer": { "version": "4.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -4722,8 +5187,9 @@ }, "node_modules/source-map-support": { "version": "0.5.20", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", + "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", "dev": true, - "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -4731,16 +5197,18 @@ }, "node_modules/source-map-support/node_modules/source-map": { "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" } }, "node_modules/spawn-wrap": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", "dev": true, - "license": "ISC", "dependencies": { "foreground-child": "^2.0.0", "is-windows": "^1.0.2", @@ -4755,12 +5223,14 @@ }, "node_modules/sprintf-js": { "version": "1.0.3", - "dev": true, - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true }, "node_modules/sshpk": { "version": "1.16.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -4772,6 +5242,11 @@ "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, "engines": { "node": ">=0.10.0" } @@ -4791,14 +5266,16 @@ }, "node_modules/string_decoder": { "version": "1.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dependencies": { "safe-buffer": "~5.1.0" } }, "node_modules/string-width": { "version": "4.2.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4810,7 +5287,8 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4829,8 +5307,9 @@ }, "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" }, @@ -4840,7 +5319,8 @@ }, "node_modules/supports-color": { "version": "7.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": { "has-flag": "^4.0.0" }, @@ -4868,7 +5348,8 @@ }, "node_modules/telnet-client": { "version": "1.4.9", - "license": "MIT", + "resolved": "https://registry.npmjs.org/telnet-client/-/telnet-client-1.4.9.tgz", + "integrity": "sha512-ryF0E3mg6am1EnCQZj7OoBnueS3l8IT7lDyDyFR8FdIshRRKBpbKjX7AUnt1ImVd43WKl/AxYE5MTkX3LjhGaQ==", "dependencies": { "bluebird": "^3.5.4" }, @@ -4887,8 +5368,9 @@ }, "node_modules/test-exclude": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, - "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -4900,8 +5382,9 @@ }, "node_modules/text-table": { "version": "0.2.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "node_modules/through": { "version": "2.3.8", @@ -4917,15 +5400,17 @@ }, "node_modules/to-fast-properties": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/to-regex-range": { "version": "5.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dependencies": { "is-number": "^7.0.0" }, @@ -4935,12 +5420,14 @@ }, "node_modules/traverse-chain": { "version": "0.1.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", + "integrity": "sha512-up6Yvai4PYKhpNp5PkYtx50m3KbwQrqDwbuZP/ItyL64YEWHAvH6Md83LFLV/GRSk/BoUVwwgUzX6SOQSbsfAg==" }, "node_modules/ts-node": { "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz", + "integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==", "dev": true, - "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.7.0", "@tsconfig/node10": "^1.0.7", @@ -4979,29 +5466,33 @@ }, "node_modules/ts-node/node_modules/diff": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, "node_modules/ts-node/node_modules/yn": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/tslib": { "version": "1.14.1", - "dev": true, - "license": "0BSD" + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "node_modules/tsutils": { "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", "dev": true, - "license": "MIT", "dependencies": { "tslib": "^1.8.1" }, @@ -5014,12 +5505,14 @@ }, "node_modules/tweetnacl": { "version": "0.14.5", - "license": "Unlicense" + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, "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" }, @@ -5029,15 +5522,17 @@ }, "node_modules/type-detect": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/type-fest": { "version": "0.20.2", - "license": "(MIT OR CC0-1.0)", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "engines": { "node": ">=10" }, @@ -5047,16 +5542,18 @@ }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", "dev": true, - "license": "MIT", "dependencies": { "is-typedarray": "^1.0.0" } }, "node_modules/typescript": { "version": "4.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", + "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5077,14 +5574,16 @@ }, "node_modules/universalify": { "version": "0.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "engines": { "node": ">= 4.0.0" } }, "node_modules/uri-js": { "version": "4.4.1", - "license": "BSD-2-Clause", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dependencies": { "punycode": "^2.1.0" } @@ -5100,7 +5599,8 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/uuid": { "version": "3.4.0", @@ -5112,13 +5612,15 @@ }, "node_modules/v8-compile-cache": { "version": "2.3.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true }, "node_modules/validate-glob-opts": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate-glob-opts/-/validate-glob-opts-1.0.2.tgz", + "integrity": "sha512-3PKjRQq/R514lUcG9OEiW0u9f7D4fP09A07kmk1JbNn2tfeQdAHhlT+A4dqERXKu2br2rrxSM3FzagaEeq9w+A==", "dev": true, - "license": "ISC", "dependencies": { "array-to-sentence": "^1.1.0", "indexed-filter": "^1.0.0", @@ -5128,18 +5630,20 @@ }, "node_modules/validate-glob-opts/node_modules/is-plain-obj": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/verror": { "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "engines": [ "node >=0.6.0" ], - "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -5148,11 +5652,14 @@ }, "node_modules/verror/node_modules/core-util-is": { "version": "1.0.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "node_modules/vscode-debugadapter": { "version": "1.49.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.49.0.tgz", + "integrity": "sha512-nhes9zaLanFcHuchytOXGsLTGpU5qkz10mC9gVchiwNuX2Bljmc6+wsNbCyE5dOxu6F0pn3f+LEJQGMU1kcnvQ==", + "deprecated": "This package has been renamed to @vscode/debugadapter, please update to the new name", "dependencies": { "mkdirp": "^1.0.4", "vscode-debugprotocol": "1.49.0" @@ -5160,18 +5667,22 @@ }, "node_modules/vscode-debugprotocol": { "version": "1.49.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.49.0.tgz", + "integrity": "sha512-3VkK3BmaqN+BGIq4lavWp9a2IC6VYgkWkkMQm6Sa5ACkhBF6ThJDrkP+/3rFE4G7F8+mM3f4bhhJhhMax2IPfg==", + "deprecated": "This package has been renamed to @vscode/debugprotocol, please update to the new name" }, "node_modules/vscode-jsonrpc": { "version": "6.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", + "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", "engines": { "node": ">=8.0.0 || >=10.0.0" } }, "node_modules/vscode-languageserver": { "version": "6.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz", + "integrity": "sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ==", "dependencies": { "vscode-languageserver-protocol": "^3.15.3" }, @@ -5181,7 +5692,8 @@ }, "node_modules/vscode-languageserver-protocol": { "version": "3.16.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", + "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", "dependencies": { "vscode-jsonrpc": "6.0.0", "vscode-languageserver-types": "3.16.0" @@ -5189,20 +5701,24 @@ }, "node_modules/vscode-languageserver-textdocument": { "version": "1.0.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.2.tgz", + "integrity": "sha512-T7uPC18+f8mYE4lbVZwb3OSmvwTZm3cuFhrdx9Bn2l11lmp3SvSuSVjy2JtvrghzjAo4G6Trqny2m9XGnFnWVA==" }, "node_modules/vscode-languageserver-types": { "version": "3.16.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", + "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==" }, "node_modules/vscode-uri": { "version": "2.1.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.2.tgz", + "integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==" }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -5215,8 +5731,9 @@ }, "node_modules/which-module": { "version": "2.0.0", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", + "dev": true }, "node_modules/word-wrap": { "version": "1.2.4", @@ -5235,8 +5752,9 @@ }, "node_modules/wrap-ansi": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -5248,12 +5766,14 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "license": "ISC" + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", "dev": true, - "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", @@ -5292,17 +5812,20 @@ }, "node_modules/y18n": { "version": "4.0.3", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true }, "node_modules/yallist": { "version": "4.0.0", - "license": "ISC" + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "dev": true, - "license": "MIT", "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", @@ -5322,8 +5845,9 @@ }, "node_modules/yargs-parser": { "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, - "license": "ISC", "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" @@ -5334,8 +5858,9 @@ }, "node_modules/yargs-unparser": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, - "license": "MIT", "dependencies": { "camelcase": "^6.0.0", "decamelize": "^4.0.0", @@ -5348,8 +5873,9 @@ }, "node_modules/yargs-unparser/node_modules/camelcase": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -5359,8 +5885,9 @@ }, "node_modules/yargs-unparser/node_modules/decamelize": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -5380,8 +5907,9 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -5393,6 +5921,8 @@ "dependencies": { "@babel/code-frame": { "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", "dev": true, "requires": { "@babel/highlight": "^7.14.5" @@ -5400,10 +5930,14 @@ }, "@babel/compat-data": { "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", + "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==", "dev": true }, "@babel/core": { "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.8.tgz", + "integrity": "sha512-3UG9dsxvYBMYwRv+gS41WKHno4K60/9GPy1CJaH6xy3Elq8CTtvtjT5R5jmNhXfCYLX2mTw+7/aq5ak/gOE0og==", "dev": true, "requires": { "@babel/code-frame": "^7.15.8", @@ -5424,17 +5958,23 @@ }, "dependencies": { "semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, "source-map": { "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "dev": true } } }, "@babel/generator": { "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz", + "integrity": "sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==", "dev": true, "requires": { "@babel/types": "^7.15.6", @@ -5444,12 +5984,16 @@ "dependencies": { "source-map": { "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "dev": true } } }, "@babel/helper-compilation-targets": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz", + "integrity": "sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ==", "dev": true, "requires": { "@babel/compat-data": "^7.15.0", @@ -5459,13 +6003,17 @@ }, "dependencies": { "semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } }, "@babel/helper-function-name": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.15.4", @@ -5475,6 +6023,8 @@ }, "@babel/helper-get-function-arity": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", "dev": true, "requires": { "@babel/types": "^7.15.4" @@ -5482,6 +6032,8 @@ }, "@babel/helper-hoist-variables": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz", + "integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==", "dev": true, "requires": { "@babel/types": "^7.15.4" @@ -5489,6 +6041,8 @@ }, "@babel/helper-member-expression-to-functions": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz", + "integrity": "sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA==", "dev": true, "requires": { "@babel/types": "^7.15.4" @@ -5496,6 +6050,8 @@ }, "@babel/helper-module-imports": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz", + "integrity": "sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==", "dev": true, "requires": { "@babel/types": "^7.15.4" @@ -5503,6 +6059,8 @@ }, "@babel/helper-module-transforms": { "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.8.tgz", + "integrity": "sha512-DfAfA6PfpG8t4S6npwzLvTUpp0sS7JrcuaMiy1Y5645laRJIp/LiLGIBbQKaXSInK8tiGNI7FL7L8UvB8gdUZg==", "dev": true, "requires": { "@babel/helper-module-imports": "^7.15.4", @@ -5517,6 +6075,8 @@ }, "@babel/helper-optimise-call-expression": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz", + "integrity": "sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw==", "dev": true, "requires": { "@babel/types": "^7.15.4" @@ -5524,6 +6084,8 @@ }, "@babel/helper-replace-supers": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz", + "integrity": "sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw==", "dev": true, "requires": { "@babel/helper-member-expression-to-functions": "^7.15.4", @@ -5534,6 +6096,8 @@ }, "@babel/helper-simple-access": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz", + "integrity": "sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg==", "dev": true, "requires": { "@babel/types": "^7.15.4" @@ -5541,6 +6105,8 @@ }, "@babel/helper-split-export-declaration": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", "dev": true, "requires": { "@babel/types": "^7.15.4" @@ -5548,14 +6114,20 @@ }, "@babel/helper-validator-identifier": { "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", "dev": true }, "@babel/helper-validator-option": { "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", + "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", "dev": true }, "@babel/helpers": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.4.tgz", + "integrity": "sha512-V45u6dqEJ3w2rlryYYXf6i9rQ5YMNu4FLS6ngs8ikblhu2VdR1AqAd6aJjBzmf2Qzh6KOLqKHxEN9+TFbAkAVQ==", "dev": true, "requires": { "@babel/template": "^7.15.4", @@ -5565,6 +6137,8 @@ }, "@babel/highlight": { "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.5", @@ -5574,6 +6148,8 @@ "dependencies": { "ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { "color-convert": "^1.9.0" @@ -5581,6 +6157,8 @@ }, "chalk": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", @@ -5590,6 +6168,8 @@ }, "color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "requires": { "color-name": "1.1.3" @@ -5597,14 +6177,20 @@ }, "color-name": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, "has-flag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, "supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { "has-flag": "^3.0.0" @@ -5614,10 +6200,14 @@ }, "@babel/parser": { "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", "dev": true }, "@babel/template": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", "dev": true, "requires": { "@babel/code-frame": "^7.14.5", @@ -5627,6 +6217,8 @@ }, "@babel/traverse": { "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", + "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", "dev": true, "requires": { "@babel/code-frame": "^7.14.5", @@ -5642,12 +6234,16 @@ "dependencies": { "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 } } }, "@babel/types": { "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.9", @@ -5656,10 +6252,14 @@ }, "@cspotcode/source-map-consumer": { "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", "dev": true }, "@cspotcode/source-map-support": { "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", "dev": true, "requires": { "@cspotcode/source-map-consumer": "0.8.0" @@ -5667,6 +6267,8 @@ }, "@eslint/eslintrc": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.3.tgz", + "integrity": "sha512-DHI1wDPoKCBPoLZA3qDR91+3te/wDSc1YhKg3jR8NxKKRJq2hwHwcWv31cSwSYvIBrmbENoYMWcenW8uproQqg==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -5682,6 +6284,8 @@ }, "@humanwhocodes/config-array": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.6.0.tgz", + "integrity": "sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.0", @@ -5691,10 +6295,14 @@ }, "@humanwhocodes/object-schema": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", + "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", "dev": true }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, "requires": { "camelcase": "^5.3.1", @@ -5706,26 +6314,36 @@ "dependencies": { "resolve-from": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true } } }, "@istanbuljs/schema": { "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true }, "@nodelib/fs.scandir": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "requires": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "@nodelib/fs.stat": { - "version": "2.0.5" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" }, "@nodelib/fs.walk": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "requires": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -5768,7 +6386,9 @@ } }, "@rokucommunity/bslib": { - "version": "0.1.1" + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@rokucommunity/bslib/-/bslib-0.1.1.tgz", + "integrity": "sha512-2ox6EUL+UTtccTbD4dbVjZK3QHa0PHCqpoKMF8lZz9ayzzEP3iVPF8KZR6hOi6bxsIcbGXVjqmtCVkpC4P9SrA==" }, "@rokucommunity/logger": { "version": "0.3.3", @@ -5783,6 +6403,8 @@ }, "@sinonjs/commons": { "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", "dev": true, "requires": { "type-detect": "4.0.8" @@ -5790,6 +6412,8 @@ }, "@sinonjs/fake-timers": { "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0" @@ -5797,6 +6421,8 @@ }, "@sinonjs/samsam": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", + "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", "dev": true, "requires": { "@sinonjs/commons": "^1.6.0", @@ -5806,31 +6432,44 @@ }, "@sinonjs/text-encoding": { "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, "@tsconfig/node10": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", "dev": true }, "@tsconfig/node12": { "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", "dev": true }, "@tsconfig/node14": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", "dev": true }, "@tsconfig/node16": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, "@types/caseless": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", + "dev": true }, "@types/chai": { "version": "4.2.22", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.22.tgz", + "integrity": "sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==", "dev": true }, "@types/dateformat": { @@ -5839,6 +6478,12 @@ "integrity": "sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g==", "dev": true }, + "@types/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==", + "dev": true + }, "@types/decompress": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.4.tgz", @@ -5850,14 +6495,20 @@ }, "@types/dedent": { "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@types/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A==", "dev": true }, "@types/find-in-files": { "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@types/find-in-files/-/find-in-files-0.5.1.tgz", + "integrity": "sha512-kUPtvVXZn99bBHx08jAJgrI1NKWspuoX6RgqQgfNlH2debcwcowUV41P6Kfg4VDaCAr5KNBW9qdjIyKRnXVuBA==", "dev": true }, "@types/fs-extra": { "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", "dev": true, "requires": { "@types/node": "*" @@ -5865,6 +6516,8 @@ }, "@types/glob": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", "dev": true, "requires": { "@types/minimatch": "*", @@ -5873,23 +6526,31 @@ }, "@types/json-schema": { "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "dev": true }, "@types/minimatch": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", "dev": true }, "@types/mocha": { "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", + "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", "dev": true }, "@types/node": { - "version": "16.11.6" + "version": "16.11.6", + "dev": true }, "@types/request": { "version": "2.48.8", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", + "dev": true, "requires": { "@types/caseless": "*", "@types/node": "*", @@ -5899,10 +6560,14 @@ }, "@types/semver": { "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz", + "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==", "dev": true }, "@types/sinon": { "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.6.tgz", + "integrity": "sha512-6EF+wzMWvBNeGrfP3Nx60hhx+FfwSg1JJBLAAP/IdIUq0EYkqCYf70VT3PhuhPX9eLD+Dp+lNdpb/ZeHG8Yezg==", "dev": true, "requires": { "@sinonjs/fake-timers": "^7.1.0" @@ -5911,14 +6576,19 @@ "@types/tough-cookie": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" + "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", + "dev": true }, "@types/vscode": { "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.61.0.tgz", + "integrity": "sha512-9k5Nwq45hkRwdfCFY+eKXeQQSbPoA114mF7U/4uJXRBJeGIO7MuJdhF1PnaDN+lllL9iKGQtd6FFXShBXMNaFg==", "dev": true }, "@typescript-eslint/eslint-plugin": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.2.0.tgz", + "integrity": "sha512-qQwg7sqYkBF4CIQSyRQyqsYvP+g/J0To9ZPVNJpfxfekl5RmdvQnFFTVVwpRtaUDFNvjfe/34TgY/dpc3MgNTw==", "dev": true, "requires": { "@typescript-eslint/experimental-utils": "5.2.0", @@ -5933,12 +6603,16 @@ "dependencies": { "ignore": { "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true } } }, "@typescript-eslint/experimental-utils": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.2.0.tgz", + "integrity": "sha512-fWyT3Agf7n7HuZZRpvUYdFYbPk3iDCq6fgu3ulia4c7yxmPnwVBovdSOX7RL+k8u6hLbrXcdAehlWUVpGh6IEw==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", @@ -5951,6 +6625,8 @@ }, "@typescript-eslint/parser": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.2.0.tgz", + "integrity": "sha512-Uyy4TjJBlh3NuA8/4yIQptyJb95Qz5PX//6p8n7zG0QnN4o3NF9Je3JHbVU7fxf5ncSXTmnvMtd/LDQWDk0YqA==", "dev": true, "requires": { "@typescript-eslint/scope-manager": "5.2.0", @@ -5961,6 +6637,8 @@ }, "@typescript-eslint/scope-manager": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.2.0.tgz", + "integrity": "sha512-RW+wowZqPzQw8MUFltfKYZfKXqA2qgyi6oi/31J1zfXJRpOn6tCaZtd9b5u9ubnDG2n/EMvQLeZrsLNPpaUiFQ==", "dev": true, "requires": { "@typescript-eslint/types": "5.2.0", @@ -5969,10 +6647,14 @@ }, "@typescript-eslint/types": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.2.0.tgz", + "integrity": "sha512-cTk6x08qqosps6sPyP2j7NxyFPlCNsJwSDasqPNjEQ8JMD5xxj2NHxcLin5AJQ8pAVwpQ8BMI3bTxR0zxmK9qQ==", "dev": true }, "@typescript-eslint/typescript-estree": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.2.0.tgz", + "integrity": "sha512-RsdXq2XmVgKbm9nLsE3mjNUM7BTr/K4DYR9WfFVMUuozHWtH5gMpiNZmtrMG8GR385EOSQ3kC9HiEMJWimxd/g==", "dev": true, "requires": { "@typescript-eslint/types": "5.2.0", @@ -5986,6 +6668,8 @@ }, "@typescript-eslint/visitor-keys": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.2.0.tgz", + "integrity": "sha512-Nk7HizaXWWCUBfLA/rPNKMzXzWS8Wg9qHMuGtT+v2/YpPij4nVXrVJc24N/r5WrrmqK31jCrZxeHqIgqRzs0Xg==", "dev": true, "requires": { "@typescript-eslint/types": "5.2.0", @@ -5994,22 +6678,30 @@ "dependencies": { "eslint-visitor-keys": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz", + "integrity": "sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q==", "dev": true } } }, "@ungap/promise-all-settled": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, "@xml-tools/parser": { "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@xml-tools/parser/-/parser-1.0.11.tgz", + "integrity": "sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==", "requires": { "chevrotain": "7.1.1" }, "dependencies": { "chevrotain": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-7.1.1.tgz", + "integrity": "sha512-wy3mC1x4ye+O+QkEinVJkPf5u2vsrDIYW9G7ZuwFl6v/Yu0LwUuT2POsb+NUWApebyxfkQq6+yDfRExbnI5rcw==", "requires": { "regexp-to-ast": "0.5.0" } @@ -6018,19 +6710,27 @@ }, "acorn": { "version": "8.5.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", + "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", "dev": true }, "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, "requires": {} }, "acorn-walk": { "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, "aggregate-error": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, "requires": { "clean-stack": "^2.0.0", @@ -6039,6 +6739,8 @@ }, "ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6048,19 +6750,27 @@ }, "ansi-colors": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true }, "ansi-regex": { - "version": "5.0.1" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { "color-convert": "^2.0.1" } }, "anymatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -6068,6 +6778,8 @@ }, "append-transform": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", "dev": true, "requires": { "default-require-extensions": "^3.0.0" @@ -6075,45 +6787,65 @@ }, "append-type": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/append-type/-/append-type-1.0.2.tgz", + "integrity": "sha512-hac740vT/SAbrFBLgLIWZqVT5PUAcGTWS5UkDDhr+OCizZSw90WKw6sWAEgGaYd2viIblggypMXwpjzHXOvAQg==", "dev": true }, "archy": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, "arg": { "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, "argparse": { "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "requires": { "sprintf-js": "~1.0.2" } }, "array-flat-polyfill": { - "version": "1.0.1" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-flat-polyfill/-/array-flat-polyfill-1.0.1.tgz", + "integrity": "sha512-hfJmKupmQN0lwi0xG6FQ5U8Rd97RnIERplymOv/qpq8AoNKPPAnxJadjFA23FNWm88wykh9HmpLJUUwUtNU/iw==" }, "array-to-sentence": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-to-sentence/-/array-to-sentence-1.1.0.tgz", + "integrity": "sha512-YkwkMmPA2+GSGvXj1s9NZ6cc2LBtR+uSeWTy2IGi5MR1Wag4DdrcjTxA/YV/Fw+qKlBeXomneZgThEbm/wvZbw==", "dev": true }, "array-union": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, "asn1": { "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", "requires": { "safer-buffer": "~2.1.0" } }, "assert-plus": { - "version": "1.0.0" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" }, "assert-valid-glob-opts": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-valid-glob-opts/-/assert-valid-glob-opts-1.0.0.tgz", + "integrity": "sha512-/mttty5Xh7wE4o7ttKaUpBJl0l04xWe3y6muy1j27gyzSsnceK0AYU9owPtUoL9z8+9hnPxztmuhdFZ7jRoyWw==", "dev": true, "requires": { "glob-option-error": "^1.0.0", @@ -6122,13 +6854,28 @@ }, "assertion-error": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, + "async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, "asynckit": { - "version": "0.4.0" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "aws-sign2": { - "version": "0.7.0" + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" }, "aws4": { "version": "1.12.0", @@ -6136,7 +6883,9 @@ "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, "balanced-match": { - "version": "1.0.2" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "base64-js": { "version": "1.5.1", @@ -6145,12 +6894,16 @@ }, "bcrypt-pbkdf": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "requires": { "tweetnacl": "^0.14.3" } }, "binary-extensions": { - "version": "2.2.0" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, "bl": { "version": "1.2.3", @@ -6163,10 +6916,14 @@ } }, "bluebird": { - "version": "3.7.2" + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6174,6 +6931,8 @@ }, "braces": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "requires": { "fill-range": "^7.0.1" } @@ -6219,12 +6978,16 @@ "dependencies": { "ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "requires": { "color-convert": "^1.9.0" } }, "chalk": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -6233,6 +6996,8 @@ }, "cliui": { "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -6241,12 +7006,16 @@ }, "color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "requires": { "color-name": "1.1.3" } }, "color-name": { - "version": "1.1.3" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "fs-extra": { "version": "8.1.0", @@ -6259,31 +7028,43 @@ } }, "has-flag": { - "version": "3.0.0" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" }, "serialize-error": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", "requires": { "type-fest": "^0.13.1" } }, "supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "requires": { "has-flag": "^3.0.0" } }, "type-fest": { - "version": "0.13.1" + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==" }, "vscode-languageserver": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", + "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", "requires": { "vscode-languageserver-protocol": "3.16.0" } }, "wrap-ansi": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -6292,26 +7073,36 @@ "dependencies": { "ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { "color-convert": "^2.0.1" } }, "color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "requires": { "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.4" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" } } }, "y18n": { - "version": "5.0.8" + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yargs": { "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -6323,7 +7114,9 @@ } }, "yargs-parser": { - "version": "20.2.9" + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" } } }, @@ -6337,10 +7130,14 @@ }, "browser-stdout": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, "browserslist": { "version": "4.17.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.5.tgz", + "integrity": "sha512-I3ekeB92mmpctWBoLXe0d5wPS2cBuRvvW0JyyJHMrk9/HmP2ZjrTboNAZ8iuGqaEIlKguljbQY32OkOJIRrgoA==", "dev": true, "requires": { "caniuse-lite": "^1.0.30001271", @@ -6390,10 +7187,14 @@ }, "buffer-from": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, "caching-transform": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", "dev": true, "requires": { "hasha": "^5.0.0", @@ -6404,10 +7205,14 @@ }, "callsites": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, "camelcase": { "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, "caniuse-lite": { @@ -6417,10 +7222,14 @@ "dev": true }, "caseless": { - "version": "0.12.0" + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, "chai": { "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", "dev": true, "requires": { "assertion-error": "^1.1.0", @@ -6433,6 +7242,8 @@ }, "chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -6440,10 +7251,14 @@ }, "check-error": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", "dev": true }, "chevrotain": { "version": "7.1.2", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-7.1.2.tgz", + "integrity": "sha512-9bQsXVQ7UAvzMs7iUBBJ9Yv//exOy7bIR3PByOEk4M64vIE/LsiOiX7VIkMF/vEMlrSStwsaE884Bp9CpjtC5g==", "requires": { "regexp-to-ast": "0.5.0" } @@ -6465,13 +7280,19 @@ }, "clean-stack": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true }, "clear": { - "version": "0.1.0" + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/clear/-/clear-0.1.0.tgz", + "integrity": "sha512-qMjRnoL+JDPJHeLePZJuao6+8orzHMGP04A8CdwCNsKhRbOnKRjefxONR7bwILT3MHecxKBjHkKL/tkZ8r4Uzw==" }, "cliui": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dev": true, "requires": { "string-width": "^4.2.0", @@ -6481,15 +7302,21 @@ }, "color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "requires": { "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.4" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "combined-stream": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "requires": { "delayed-stream": "~1.0.0" } @@ -6502,20 +7329,28 @@ }, "commondir": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, "concat-map": { - "version": "0.0.1" + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "convert-source-map": { "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", "dev": true, "requires": { "safe-buffer": "~5.1.1" } }, "core-util-is": { - "version": "1.0.3" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "coveralls-next": { "version": "4.2.0", @@ -6556,13 +7391,19 @@ }, "create-require": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, "cross-platform-clear-console": { - "version": "2.3.0" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cross-platform-clear-console/-/cross-platform-clear-console-2.3.0.tgz", + "integrity": "sha512-To+sJ6plHHC6k5DfdvSVn6F1GRGJh/R6p76bCpLbyMyHEmbqFyuMAeGwDcz/nGDWH3HUcjFTTX9iUSCzCg9Eiw==" }, "cross-spawn": { "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -6572,6 +7413,8 @@ }, "dashdash": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "requires": { "assert-plus": "^1.0.0" } @@ -6586,8 +7429,15 @@ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" }, + "debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, "debounce-promise": { - "version": "3.1.2" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz", + "integrity": "sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg==" }, "debug": { "version": "4.3.3", @@ -6600,6 +7450,8 @@ }, "decamelize": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true }, "decompress": { @@ -6724,10 +7576,14 @@ }, "dedent": { "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, "deep-eql": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", "dev": true, "requires": { "type-detect": "^4.0.0" @@ -6735,10 +7591,14 @@ }, "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 }, "default-require-extensions": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", "dev": true, "requires": { "strip-bom": "^4.0.0" @@ -6746,19 +7606,27 @@ "dependencies": { "strip-bom": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true } } }, "delayed-stream": { - "version": "1.0.0" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "diff": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true }, "dir-glob": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, "requires": { "path-type": "^4.0.0" @@ -6766,6 +7634,8 @@ }, "doctrine": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "requires": { "esutils": "^2.0.2" @@ -6773,6 +7643,8 @@ }, "ecc-jsbn": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -6780,10 +7652,14 @@ }, "electron-to-chromium": { "version": "1.3.880", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.880.tgz", + "integrity": "sha512-iwIP/6WoeSimzUKJIQtjtpVDsK8Ir8qQCMXsUBwg+rxJR2Uh3wTNSbxoYRfs+3UWx/9MAnPIxVZCyWkm8MT0uw==", "dev": true }, "emoji-regex": { - "version": "8.0.0" + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "end-of-stream": { "version": "1.4.4", @@ -6796,26 +7672,38 @@ }, "enquirer": { "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", "dev": true, "requires": { "ansi-colors": "^4.1.1" } }, "eol": { - "version": "0.9.1" + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==" }, "es6-error": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, "escalade": { - "version": "3.1.1" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, "escape-string-regexp": { - "version": "1.0.5" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" }, "eslint": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.1.0.tgz", + "integrity": "sha512-JZvNneArGSUsluHWJ8g8MMs3CfIEzwaLx9KyH4tZ2i+R2/rPWzL8c0zg3rHdwYVpN/1sB9gqnjHwz9HoeJpGHw==", "dev": true, "requires": { "@eslint/eslintrc": "^1.0.3", @@ -6860,14 +7748,20 @@ "dependencies": { "argparse": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, "escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, "eslint-scope": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-6.0.0.tgz", + "integrity": "sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -6876,14 +7770,20 @@ }, "eslint-visitor-keys": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz", + "integrity": "sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q==", "dev": true }, "estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, "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, "requires": { "is-glob": "^4.0.3" @@ -6891,6 +7791,8 @@ }, "js-yaml": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -6900,10 +7802,14 @@ }, "eslint-plugin-no-only-tests": { "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-2.6.0.tgz", + "integrity": "sha512-T9SmE/g6UV1uZo1oHAqOvL86XWl7Pl2EpRpnLI8g/bkJu+h7XBCB+1LnubRZ2CUQXj805vh4/CYZdnqtVaEo2Q==", "dev": true }, "eslint-scope": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -6912,6 +7818,8 @@ }, "eslint-utils": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", "dev": true, "requires": { "eslint-visitor-keys": "^2.0.0" @@ -6919,10 +7827,14 @@ }, "eslint-visitor-keys": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true }, "espree": { "version": "9.0.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.0.0.tgz", + "integrity": "sha512-r5EQJcYZ2oaGbeR0jR0fFVijGOcwai07/690YRXLINuhmVeRY4UKSAsQPe/0BNuDgwP7Ophoc1PRsr2E3tkbdQ==", "dev": true, "requires": { "acorn": "^8.5.0", @@ -6932,16 +7844,22 @@ "dependencies": { "eslint-visitor-keys": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz", + "integrity": "sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q==", "dev": true } } }, "esprima": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, "esquery": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -6949,12 +7867,16 @@ "dependencies": { "estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true } } }, "esrecurse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "requires": { "estraverse": "^5.2.0" @@ -6962,29 +7884,43 @@ "dependencies": { "estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true } } }, "estraverse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true }, "esutils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, "eventemitter3": { - "version": "4.0.7" + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, "extend": { - "version": "3.0.2" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extsprintf": { - "version": "1.3.0" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" }, "fast-deep-equal": { - "version": "3.1.3" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { "version": "3.2.12", @@ -6999,14 +7935,20 @@ } }, "fast-json-stable-stringify": { - "version": "2.1.0" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "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 }, "fastq": { "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", "requires": { "reusify": "^1.0.4" } @@ -7022,6 +7964,8 @@ }, "file-entry-cache": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "requires": { "flat-cache": "^3.0.4" @@ -7034,22 +7978,30 @@ "dev": true }, "file-url": { - "version": "3.0.0" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/file-url/-/file-url-3.0.0.tgz", + "integrity": "sha512-g872QGsHexznxkIAdK8UiZRe7SkE6kvylShU4Nsj8NvfvZag7S0QuQ4IgvPDkk75HxgjIVDwycFTDAgIiO4nDA==" }, "fill-range": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "requires": { "to-regex-range": "^5.0.1" } }, "find": { "version": "0.1.7", + "resolved": "https://registry.npmjs.org/find/-/find-0.1.7.tgz", + "integrity": "sha512-jPrupTOe/pO//3a9Ty2o4NqQCp0L46UG+swUnfFtdmtQVN8pEltKpAqR7Nuf6vWn0GBXx5w+R1MyZzqwjEIqdA==", "requires": { "traverse-chain": "~0.1.0" } }, "find-cache-dir": { "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "dev": true, "requires": { "commondir": "^1.0.1", @@ -7059,6 +8011,8 @@ "dependencies": { "pkg-dir": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "requires": { "find-up": "^4.0.0" @@ -7068,6 +8022,8 @@ }, "find-in-files": { "version": "0.5.0", + "resolved": "https://registry.npmjs.org/find-in-files/-/find-in-files-0.5.0.tgz", + "integrity": "sha512-VraTc6HdtdSHmAp0yJpAy20yPttGKzyBWc7b7FPnnsX9TOgmKx0g9xajizpF/iuu4IvNK4TP0SpyBT9zAlwG+g==", "requires": { "find": "^0.1.5", "q": "^1.0.1" @@ -7075,6 +8031,8 @@ }, "find-up": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "requires": { "locate-path": "^5.0.0", @@ -7083,10 +8041,14 @@ }, "flat": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true }, "flat-cache": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "dev": true, "requires": { "flatted": "^3.1.0", @@ -7099,6 +8061,8 @@ }, "foreground-child": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", "dev": true, "requires": { "cross-spawn": "^7.0.0", @@ -7106,12 +8070,15 @@ } }, "forever-agent": { - "version": "0.6.1" + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" }, "form-data": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -7120,6 +8087,8 @@ }, "fromentries": { "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", "dev": true }, "fs-constants": { @@ -7130,6 +8099,8 @@ }, "fs-extra": { "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", "requires": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -7138,44 +8109,64 @@ "dependencies": { "jsonfile": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "requires": { "graceful-fs": "^4.1.6", "universalify": "^2.0.0" } }, "universalify": { - "version": "2.0.0" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" } } }, "fs.realpath": { - "version": "1.0.0" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "fsevents": { "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "optional": true }, "functional-red-black-tree": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, "gensync": { "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, "get-caller-file": { - "version": "2.0.5" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-func-name": { - "version": "2.0.0", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true }, "get-package-type": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, "get-port": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", "dev": true }, "get-stream": { @@ -7190,12 +8181,16 @@ }, "getpass": { "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "requires": { "assert-plus": "^1.0.0" } }, "glob": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -7207,16 +8202,22 @@ }, "glob-option-error": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glob-option-error/-/glob-option-error-1.0.0.tgz", + "integrity": "sha512-AD7lbWbwF2Ii9gBQsQIOEzwuqP/jsnyvK27/3JDq1kn/JyfDtYI6AWz3ZQwcPuQdHSBcFh+A2yT/SEep27LOGg==", "dev": true }, "glob-parent": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "requires": { "is-glob": "^4.0.1" } }, "globals": { "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -7224,6 +8225,8 @@ }, "globby": { "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", "dev": true, "requires": { "array-union": "^2.1.0", @@ -7236,32 +8239,46 @@ "dependencies": { "ignore": { "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true } } }, "graceful-fs": { - "version": "4.2.8" + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" }, "growl": { "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, "har-schema": { - "version": "2.0.0" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" }, "har-validator": { "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "requires": { "ajv": "^6.12.3", "har-schema": "^2.0.0" } }, "has-flag": { - "version": "4.0.0" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "hasha": { "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", "dev": true, "requires": { "is-stream": "^2.0.0", @@ -7270,12 +8287,16 @@ "dependencies": { "type-fest": { "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true } } }, "he": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, "html-escaper": { @@ -7290,13 +8311,19 @@ }, "ignore": { "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, "immediate": { - "version": "3.0.6" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" }, "import-fresh": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "requires": { "parent-module": "^1.0.0", @@ -7305,14 +8332,20 @@ }, "imurmurhash": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, "indent-string": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, "indexed-filter": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/indexed-filter/-/indexed-filter-1.0.3.tgz", + "integrity": "sha512-oBIzs6EARNMzrLgVg20fK52H19WcRHBiukiiEkw9rnnI//8rinEBMLrYdwEfJ9d4K7bjV1L6nSGft6H/qzHNgQ==", "dev": true, "requires": { "append-type": "^1.0.1" @@ -7320,16 +8353,22 @@ }, "inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "requires": { "once": "^1.3.0", "wrappy": "1" } }, "inherits": { - "version": "2.0.4" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "inspect-with-kind": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz", + "integrity": "sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==", "dev": true, "requires": { "kind-of": "^6.0.2" @@ -7337,18 +8376,26 @@ }, "is-binary-path": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "requires": { "binary-extensions": "^2.0.0" } }, "is-extglob": { - "version": "2.1.1" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" }, "is-fullwidth-code-point": { - "version": "3.0.0" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-glob": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "requires": { "is-extglob": "^2.1.1" } @@ -7360,43 +8407,65 @@ "dev": true }, "is-number": { - "version": "7.0.0" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "is-plain-obj": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true }, "is-stream": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true }, "is-typedarray": { - "version": "1.0.0" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "is-unicode-supported": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true }, "is-windows": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, "isarray": { - "version": "1.0.0" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, "isstream": { - "version": "0.1.2" + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "istanbul-lib-coverage": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", "dev": true }, "istanbul-lib-hook": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", "dev": true, "requires": { "append-transform": "^2.0.0" @@ -7404,6 +8473,8 @@ }, "istanbul-lib-instrument": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "requires": { "@babel/core": "^7.7.5", @@ -7413,13 +8484,17 @@ }, "dependencies": { "semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } }, "istanbul-lib-processinfo": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", "dev": true, "requires": { "archy": "^1.0.0", @@ -7433,6 +8508,8 @@ }, "istanbul-lib-report": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", "dev": true, "requires": { "istanbul-lib-coverage": "^3.0.0", @@ -7442,6 +8519,8 @@ }, "istanbul-lib-source-maps": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, "requires": { "debug": "^4.1.1", @@ -7451,12 +8530,16 @@ "dependencies": { "source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true } } }, "istanbul-reports": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.5.tgz", + "integrity": "sha512-5+19PlhnGabNWB7kOFnuxT8H3T/iIyQzIbQMxXsURmmvKg86P2sbkrGOT77VnHw0Qr0gc2XzRaRfMZYYbSQCJQ==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -7465,10 +8548,14 @@ }, "js-tokens": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "js-yaml": { "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -7476,24 +8563,36 @@ } }, "jsbn": { - "version": "0.1.1" + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, "jsesc": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, "json-schema": { - "version": "0.4.0" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" }, "json-schema-traverse": { - "version": "0.4.1" + "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==" }, "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 }, "json-stringify-safe": { - "version": "5.0.1" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, "json5": { "version": "2.2.3", @@ -7502,10 +8601,14 @@ "dev": true }, "jsonc-parser": { - "version": "2.3.1" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==" }, "jsonfile": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "requires": { "graceful-fs": "^4.1.6" } @@ -7523,10 +8626,14 @@ }, "just-extend": { "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, "kind-of": { "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 }, "lcov-parse": { @@ -7534,6 +8641,8 @@ }, "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, "requires": { "prelude-ls": "^1.2.1", @@ -7542,27 +8651,43 @@ }, "lie": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "requires": { "immediate": "~3.0.5" } }, "locate-path": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "requires": { "p-locate": "^4.1.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "lodash.flattendeep": { "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, "lodash.get": { "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, "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 }, "log-driver": { @@ -7570,6 +8695,8 @@ }, "log-symbols": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "requires": { "chalk": "^4.1.0", @@ -7577,10 +8704,14 @@ } }, "long": { - "version": "3.2.0" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "integrity": "sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg==" }, "lru-cache": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "requires": { "yallist": "^4.0.0" } @@ -7592,26 +8723,36 @@ }, "make-dir": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "requires": { "semver": "^6.0.0" }, "dependencies": { "semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } }, "make-error": { "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, "merge2": { - "version": "1.4.1" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, "micromatch": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "requires": { "braces": "^3.0.1", "picomatch": "^2.2.3" @@ -7644,7 +8785,9 @@ "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" }, "mkdirp": { - "version": "1.0.4" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, "mocha": { "version": "9.2.2", @@ -7680,10 +8823,14 @@ "dependencies": { "argparse": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, "cliui": { "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "requires": { "string-width": "^4.2.0", @@ -7693,10 +8840,14 @@ }, "escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, "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, "requires": { "locate-path": "^6.0.0", @@ -7705,6 +8856,8 @@ }, "js-yaml": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -7712,6 +8865,8 @@ }, "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, "requires": { "p-locate": "^5.0.0" @@ -7728,10 +8883,14 @@ }, "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 }, "p-limit": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "requires": { "yocto-queue": "^0.1.0" @@ -7739,6 +8898,8 @@ }, "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, "requires": { "p-limit": "^3.0.2" @@ -7746,6 +8907,8 @@ }, "supports-color": { "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -7753,6 +8916,8 @@ }, "wrap-ansi": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { "ansi-styles": "^4.0.0", @@ -7762,10 +8927,14 @@ }, "y18n": { "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, "yargs": { "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { "cliui": "^7.0.2", @@ -7779,6 +8948,8 @@ }, "yargs-parser": { "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", "dev": true } } @@ -7790,6 +8961,8 @@ }, "ms": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "nanoid": { @@ -7800,13 +8973,19 @@ }, "natural-compare": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, "natural-orderby": { - "version": "2.0.3" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-2.0.3.tgz", + "integrity": "sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q==" }, "nise": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", + "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0", @@ -7818,6 +8997,8 @@ }, "node-preload": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", "dev": true, "requires": { "process-on-spawn": "^1.0.0" @@ -7825,13 +9006,19 @@ }, "node-releases": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", + "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", "dev": true }, "normalize-path": { - "version": "3.0.0" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "nyc": { "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", "dev": true, "requires": { "@istanbuljs/load-nyc-config": "^1.0.0", @@ -7865,12 +9052,16 @@ "dependencies": { "resolve-from": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true } } }, "oauth-sign": { - "version": "0.9.0" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "object-assign": { "version": "4.1.1", @@ -7880,12 +9071,16 @@ }, "once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "requires": { "wrappy": "1" } }, "optionator": { "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", "dev": true, "requires": { "deep-is": "^0.1.3", @@ -7898,10 +9093,14 @@ }, "p-defer": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-4.0.0.tgz", + "integrity": "sha512-Vb3QRvQ0Y5XnF40ZUWW7JfLogicVh/EnA5gBIvKDJoYpeI82+1E3AlB9yOcKFS0AhHrWVnAQO39fbR0G99IVEQ==", "dev": true }, "p-limit": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -7909,6 +9108,8 @@ }, "p-locate": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "requires": { "p-limit": "^2.2.0" @@ -7916,16 +9117,22 @@ }, "p-map": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", "dev": true, "requires": { "aggregate-error": "^3.0.0" } }, "p-reflect": { - "version": "1.0.0" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reflect/-/p-reflect-1.0.0.tgz", + "integrity": "sha512-rlngKS+EX3nvI7xIzA0xKNVEAguWdIqAZVbn02z1m73ehXBdX66aTdD0bCvIu0cDwbU3TK9w3RYrppKpO3EnKQ==" }, "p-settle": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-settle/-/p-settle-2.1.0.tgz", + "integrity": "sha512-NHFIUYc+fQTFRrzzAugq0l1drwi57PB522smetcY8C/EoTYs6cU/fC6TJj0N3rq5NhhJJbhf0VGWziL3jZDnjA==", "requires": { "p-limit": "^1.2.0", "p-reflect": "^1.0.0" @@ -7933,21 +9140,29 @@ "dependencies": { "p-limit": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "requires": { "p-try": "^1.0.0" } }, "p-try": { - "version": "1.0.0" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==" } } }, "p-try": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, "package-hash": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", "dev": true, "requires": { "graceful-fs": "^4.1.15", @@ -7957,31 +9172,45 @@ } }, "pako": { - "version": "1.0.11" + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "parent-module": { "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, "requires": { "callsites": "^3.0.0" } }, "parse-ms": { - "version": "2.1.0" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==" }, "path-exists": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, "path-is-absolute": { - "version": "1.0.1" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, "path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, "path-to-regexp": { "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", "dev": true, "requires": { "isarray": "0.0.1" @@ -7989,16 +9218,22 @@ "dependencies": { "isarray": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", "dev": true } } }, "path-type": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, "pathval": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true }, "pend": { @@ -8008,14 +9243,20 @@ "dev": true }, "performance-now": { - "version": "2.1.0" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "picocolors": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, "picomatch": { - "version": "2.3.0" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" }, "pify": { "version": "2.3.0", @@ -8038,6 +9279,37 @@ "pinkie": "^2.0.0" } }, + "portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "dev": true, + "requires": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + } + } + }, "postman-request": { "version": "2.88.1-postman.32", "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.32.tgz", @@ -8097,13 +9369,19 @@ }, "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 }, "process-nextick-args": { - "version": "2.0.1" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "process-on-spawn": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", "dev": true, "requires": { "fromentries": "^1.2.0" @@ -8111,16 +9389,24 @@ }, "progress": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, "psl": { - "version": "1.8.0" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, "punycode": { - "version": "2.1.1" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "q": { - "version": "1.5.1" + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==" }, "qs": { "version": "6.5.3", @@ -8133,10 +9419,14 @@ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" }, "queue-microtask": { - "version": "1.2.3" + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, "randombytes": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "requires": { "safe-buffer": "^5.1.0" @@ -8144,6 +9434,8 @@ }, "readable-stream": { "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -8168,14 +9460,20 @@ "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==" }, "regexp-to-ast": { - "version": "0.5.0" + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==" }, "regexpp": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, "release-zalgo": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", "dev": true, "requires": { "es6-error": "^4.0.1" @@ -8183,6 +9481,8 @@ }, "replace-in-file": { "version": "6.3.2", + "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.3.2.tgz", + "integrity": "sha512-Dbt5pXKvFVPL3WAaEB3ZX+95yP0CeAtIPJDwYzHbPP5EAHn+0UoegH/Wg3HKflU9dYBH8UnBC2NvY3P+9EZtTg==", "requires": { "chalk": "^4.1.2", "glob": "^7.2.0", @@ -8191,6 +9491,8 @@ "dependencies": { "cliui": { "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -8199,6 +9501,8 @@ }, "wrap-ansi": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8206,10 +9510,14 @@ } }, "y18n": { - "version": "5.0.8" + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yargs": { "version": "17.2.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz", + "integrity": "sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==", "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -8221,18 +9529,26 @@ } }, "yargs-parser": { - "version": "20.2.9" + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" } } }, "replace-last": { - "version": "1.2.6" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/replace-last/-/replace-last-1.2.6.tgz", + "integrity": "sha512-Cj+MK38VtNu1S5J73mEZY3ciQb9dJajNq1Q8inP4dn/MhJMjHwoAF3Z3FjspwAEV9pfABl565MQucmrjOkty4g==" }, "require-directory": { - "version": "2.1.1" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, "require-main-filename": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, "require-relative": { @@ -8247,13 +9563,19 @@ }, "resolve-from": { "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 }, "reusify": { - "version": "1.0.4" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, "rimraf": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -8261,6 +9583,8 @@ }, "rmfr": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rmfr/-/rmfr-2.0.0.tgz", + "integrity": "sha512-nQptLCZeyyJfgbpf2x97k5YE8vzDn7bhwx9NlvODdhgbU0mL1ruh71X0HYdRaOEvWC7Cr+SfV0p5p+Ib5yOl7A==", "dev": true, "requires": { "assert-valid-glob-opts": "^1.0.0", @@ -8272,6 +9596,8 @@ "dependencies": { "rimraf": { "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dev": true, "requires": { "glob": "^7.1.3" @@ -8302,12 +9628,16 @@ "dependencies": { "ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "requires": { "color-convert": "^1.9.0" } }, "chalk": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -8316,18 +9646,26 @@ }, "color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "requires": { "color-name": "1.1.3" } }, "color-name": { - "version": "1.1.3" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "dateformat": { - "version": "3.0.3" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" }, "fs-extra": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "requires": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8335,10 +9673,14 @@ } }, "has-flag": { - "version": "3.0.0" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" }, "supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "requires": { "has-flag": "^3.0.0" } @@ -8347,12 +9689,16 @@ }, "run-parallel": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "requires": { "queue-microtask": "^1.2.2" } }, "rxjs": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz", + "integrity": "sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==", "dev": true, "requires": { "tslib": "~2.1.0" @@ -8360,18 +9706,26 @@ "dependencies": { "tslib": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", "dev": true } } }, "safe-buffer": { - "version": "5.1.2" + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-json-stringify": { - "version": "1.2.0" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==" }, "safer-buffer": { - "version": "2.1.2" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sax": { "version": "1.2.4", @@ -8388,21 +9742,25 @@ } }, "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } }, "serialize-error": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", "requires": { "type-fest": "^0.20.2" } }, "serialize-javascript": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -8410,6 +9768,8 @@ }, "set-blocking": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true }, "setimmediate": { @@ -8419,6 +9779,8 @@ }, "shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { "shebang-regex": "^3.0.0" @@ -8426,14 +9788,20 @@ }, "shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, "signal-exit": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", + "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", "dev": true }, "sinon": { "version": "11.1.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz", + "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==", "dev": true, "requires": { "@sinonjs/commons": "^1.8.3", @@ -8446,10 +9814,14 @@ }, "slash": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, "smart-buffer": { - "version": "4.2.0" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" }, "source-map": { "version": "0.7.4", @@ -8458,6 +9830,8 @@ }, "source-map-support": { "version": "0.5.20", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", + "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -8466,12 +9840,16 @@ "dependencies": { "source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true } } }, "spawn-wrap": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", "dev": true, "requires": { "foreground-child": "^2.0.0", @@ -8484,10 +9862,14 @@ }, "sprintf-js": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, "sshpk": { "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -8517,12 +9899,16 @@ }, "string_decoder": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" } }, "string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -8531,6 +9917,8 @@ }, "strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "requires": { "ansi-regex": "^5.0.1" } @@ -8546,10 +9934,14 @@ }, "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 }, "supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "requires": { "has-flag": "^4.0.0" } @@ -8571,6 +9963,8 @@ }, "telnet-client": { "version": "1.4.9", + "resolved": "https://registry.npmjs.org/telnet-client/-/telnet-client-1.4.9.tgz", + "integrity": "sha512-ryF0E3mg6am1EnCQZj7OoBnueS3l8IT7lDyDyFR8FdIshRRKBpbKjX7AUnt1ImVd43WKl/AxYE5MTkX3LjhGaQ==", "requires": { "bluebird": "^3.5.4" } @@ -8582,6 +9976,8 @@ }, "test-exclude": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, "requires": { "@istanbuljs/schema": "^0.1.2", @@ -8591,6 +9987,8 @@ }, "text-table": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, "through": { @@ -8607,19 +10005,27 @@ }, "to-fast-properties": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true }, "to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "requires": { "is-number": "^7.0.0" } }, "traverse-chain": { - "version": "0.1.0" + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", + "integrity": "sha512-up6Yvai4PYKhpNp5PkYtx50m3KbwQrqDwbuZP/ItyL64YEWHAvH6Md83LFLV/GRSk/BoUVwwgUzX6SOQSbsfAg==" }, "ts-node": { "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz", + "integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==", "dev": true, "requires": { "@cspotcode/source-map-support": "0.7.0", @@ -8638,30 +10044,42 @@ "dependencies": { "diff": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, "yn": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true } } }, "tslib": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, "tsutils": { "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", "dev": true, "requires": { "tslib": "^1.8.1" } }, "tweetnacl": { - "version": "0.14.5" + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, "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, "requires": { "prelude-ls": "^1.2.1" @@ -8669,13 +10087,19 @@ }, "type-detect": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, "type-fest": { - "version": "0.20.2" + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" }, "typedarray-to-buffer": { "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", "dev": true, "requires": { "is-typedarray": "^1.0.0" @@ -8683,6 +10107,8 @@ }, "typescript": { "version": "4.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", + "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", "dev": true }, "unbzip2-stream": { @@ -8696,10 +10122,14 @@ } }, "universalify": { - "version": "0.1.2" + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" }, "uri-js": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "requires": { "punycode": "^2.1.0" } @@ -8714,7 +10144,9 @@ } }, "util-deprecate": { - "version": "1.0.2" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "uuid": { "version": "3.4.0", @@ -8722,10 +10154,14 @@ }, "v8-compile-cache": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, "validate-glob-opts": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate-glob-opts/-/validate-glob-opts-1.0.2.tgz", + "integrity": "sha512-3PKjRQq/R514lUcG9OEiW0u9f7D4fP09A07kmk1JbNn2tfeQdAHhlT+A4dqERXKu2br2rrxSM3FzagaEeq9w+A==", "dev": true, "requires": { "array-to-sentence": "^1.1.0", @@ -8736,12 +10172,16 @@ "dependencies": { "is-plain-obj": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true } } }, "verror": { "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -8749,47 +10189,67 @@ }, "dependencies": { "core-util-is": { - "version": "1.0.2" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" } } }, "vscode-debugadapter": { "version": "1.49.0", + "resolved": "https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.49.0.tgz", + "integrity": "sha512-nhes9zaLanFcHuchytOXGsLTGpU5qkz10mC9gVchiwNuX2Bljmc6+wsNbCyE5dOxu6F0pn3f+LEJQGMU1kcnvQ==", "requires": { "mkdirp": "^1.0.4", "vscode-debugprotocol": "1.49.0" } }, "vscode-debugprotocol": { - "version": "1.49.0" + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.49.0.tgz", + "integrity": "sha512-3VkK3BmaqN+BGIq4lavWp9a2IC6VYgkWkkMQm6Sa5ACkhBF6ThJDrkP+/3rFE4G7F8+mM3f4bhhJhhMax2IPfg==" }, "vscode-jsonrpc": { - "version": "6.0.0" + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", + "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==" }, "vscode-languageserver": { "version": "6.1.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz", + "integrity": "sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ==", "requires": { "vscode-languageserver-protocol": "^3.15.3" } }, "vscode-languageserver-protocol": { "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", + "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", "requires": { "vscode-jsonrpc": "6.0.0", "vscode-languageserver-types": "3.16.0" } }, "vscode-languageserver-textdocument": { - "version": "1.0.2" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.2.tgz", + "integrity": "sha512-T7uPC18+f8mYE4lbVZwb3OSmvwTZm3cuFhrdx9Bn2l11lmp3SvSuSVjy2JtvrghzjAo4G6Trqny2m9XGnFnWVA==" }, "vscode-languageserver-types": { - "version": "3.16.0" + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", + "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==" }, "vscode-uri": { - "version": "2.1.2" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.2.tgz", + "integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==" }, "which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "requires": { "isexe": "^2.0.0" @@ -8797,6 +10257,8 @@ }, "which-module": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", "dev": true }, "word-wrap": { @@ -8813,6 +10275,8 @@ }, "wrap-ansi": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "requires": { "ansi-styles": "^4.0.0", @@ -8821,10 +10285,14 @@ } }, "wrappy": { - "version": "1.0.2" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "write-file-atomic": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", "dev": true, "requires": { "imurmurhash": "^0.1.4", @@ -8855,13 +10323,19 @@ }, "y18n": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, "yallist": { - "version": "4.0.0" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "dev": true, "requires": { "cliui": "^6.0.0", @@ -8879,6 +10353,8 @@ }, "yargs-parser": { "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -8887,6 +10363,8 @@ }, "yargs-unparser": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "requires": { "camelcase": "^6.0.0", @@ -8897,10 +10375,14 @@ "dependencies": { "camelcase": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", "dev": true }, "decamelize": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true } } @@ -8917,6 +10399,8 @@ }, "yocto-queue": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true } } diff --git a/package.json b/package.json index c5555590..aaf6b455 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,9 @@ "preversion": "npm run build && npm run lint && npm run test", "lint": "eslint \"src/**\"", "watch": "tsc --watch", - "test": "nyc mocha \"src/**/*spec.ts\"", - "test:nocover": "mocha \"src/**/*.spec.ts\"", + "test": "nyc mocha \"src/**/*spec.ts\" --exclude \"src/**/*.device.spec.ts\"", + "device-test": "mocha --spec \"src/**/*.device.spec.ts\"", + "test:nocover": "mocha \"src/**/*.spec.ts\" --exclude \"src/**/*.device.spec.ts\"", "publish-coverage": "nyc report --reporter=text-lcov | coveralls" }, "typings": "dist/index.d.ts", @@ -24,6 +25,10 @@ "source-map-support/register", "ts-node/register" ], + "watchFiles": [ + "src/**/*" + ], + "timeout": 2000, "fullTrace": true, "watchExtensions": [ "ts" @@ -55,6 +60,7 @@ "devDependencies": { "@types/chai": "^4.2.22", "@types/dateformat": "~3", + "@types/debounce": "^1.2.1", "@types/decompress": "^4.2.4", "@types/dedent": "^0.7.0", "@types/find-in-files": "^0.5.1", @@ -62,6 +68,7 @@ "@types/glob": "^7.2.0", "@types/mocha": "^9.0.0", "@types/node": "^16.11.6", + "@types/request": "^2.48.8", "@types/semver": "^7.3.9", "@types/sinon": "^10.0.6", "@types/vscode": "^1.61.0", @@ -77,6 +84,7 @@ "mocha": "^9.1.3", "nyc": "^15.1.0", "p-defer": "^4.0.0", + "portfinder": "^1.0.32", "rimraf": "^3.0.2", "rmfr": "^2.0.0", "rxjs": "^7.4.0", @@ -89,7 +97,8 @@ "@rokucommunity/logger": "^0.3.3", "@types/request": "^2.48.8", "brighterscript": "^0.65.8", - "dateformat": "~4", + "dateformat": "^4.6.3", + "debounce": "^1.2.1", "eol": "^0.9.1", "eventemitter3": "^4.0.7", "fast-glob": "^3.2.11", @@ -100,7 +109,7 @@ "replace-in-file": "^6.3.2", "replace-last": "^1.2.6", "roku-deploy": "^3.10.3", - "semver": "^7.5.3", + "semver": "^7.5.4", "serialize-error": "^8.1.0", "smart-buffer": "^4.2.0", "source-map": "^0.7.4", diff --git a/src/CompileErrorProcessor.spec.ts b/src/CompileErrorProcessor.spec.ts index 1caabf12..c8b8e5c7 100644 --- a/src/CompileErrorProcessor.spec.ts +++ b/src/CompileErrorProcessor.spec.ts @@ -3,7 +3,8 @@ import { CompileErrorProcessor, CompileStatus } from './CompileErrorProcessor'; import { expect } from 'chai'; import type { SinonFakeTimers } from 'sinon'; import { createSandbox } from 'sinon'; -import { util as bscUtil } from 'brighterscript'; +import { DiagnosticSeverity, util as bscUtil } from 'brighterscript'; +import dedent = require('dedent'); const sinon = createSandbox(); describe('CompileErrorProcessor', () => { @@ -73,7 +74,26 @@ describe('CompileErrorProcessor', () => { describe('sendErrors', () => { it('emits the errors', async () => { - compiler.processUnhandledLines(`-------> Error parsing XML component SimpleButton.xml`); + compiler.processUnhandledLines(dedent` + 10-05 18:03:33.677 [beacon.signal] |AppCompileInitiate --------> TimeBase(0 ms) + 10-05 18:03:33.679 [scrpt.cmpl] Compiling 'app', id 'dev' + 10-05 18:03:33.681 [scrpt.load.mkup] Loading markup dev 'app' + 10-05 18:03:33.681 [scrpt.unload.mkup] Unloading markup dev 'app' + 10-05 18:03:33.683 [scrpt.parse.mkup.time] Parsed markup dev 'app' in 1 milliseconds + + ------ Compiling dev 'app' ------ + + ================================================================= + Found 1 compile error + --- Syntax Error. (compile error &h02) in pkg:/components/MainScene.brs(3) + *** ERROR compiling MainScene: + + + ================================================================= + An error occurred while attempting to compile the application's components: + -------> Compilation Failed. + MainScene + `); let callCount = 0; compiler.on('diagnostics', () => { callCount++; @@ -113,7 +133,8 @@ describe('CompileErrorProcessor', () => { path: 'SimpleButton.xml', range: bscUtil.createRange(0, 0, 0, 999), message: `Error parsing XML component`, - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }]); }); @@ -124,7 +145,8 @@ describe('CompileErrorProcessor', () => { path: 'pkg:/components/SimpleButton.xml', range: bscUtil.createRange(0, 0, 0, 999), message: `Error parsing XML component`, - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }]); }); }); @@ -140,7 +162,8 @@ describe('CompileErrorProcessor', () => { range: bscUtil.createRange(0, 0, 0, 999), message: 'Error in XML component RedButton', path: 'pkg:/components/RedButton.xml', - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }]); }); @@ -152,7 +175,8 @@ describe('CompileErrorProcessor', () => { path: 'Parsers.brs', range: bscUtil.createRange(18, 0, 18, 999), message: `Invalid #If/#ElseIf expression ( not defined) 'BAD_BS_CONST'`, - code: '&h92' + code: '&h92', + severity: DiagnosticSeverity.Error }]); }); @@ -163,7 +187,8 @@ describe('CompileErrorProcessor', () => { path: 'Parsers.brs', range: bscUtil.createRange(18, 0, 18, 999), message: `Invalid #If/#ElseIf expression ( not defined) 'BAD_BS_CONST'`, - code: '&h92' + code: '&h92', + severity: DiagnosticSeverity.Error }]); }); @@ -174,7 +199,8 @@ describe('CompileErrorProcessor', () => { path: 'Parsers.brs', range: bscUtil.createRange(18, 0, 18, 999), message: `Invalid #If/#ElseIf expression ( not defined)`, - code: '&h92' + code: '&h92', + severity: DiagnosticSeverity.Error }]); }); @@ -185,7 +211,8 @@ describe('CompileErrorProcessor', () => { path: 'Parsers.brs', range: bscUtil.createRange(0, 0, 0, 999), message: `Invalid #If/#ElseIf expression ( not defined) 'BAD_BS_CONST'`, - code: '&h92' + code: '&h92', + severity: DiagnosticSeverity.Error }]); }); @@ -196,7 +223,8 @@ describe('CompileErrorProcessor', () => { path: 'Parsers.brs', range: bscUtil.createRange(18, 0, 18, 999), message: `Invalid #If/#ElseIf expression ( not defined) 'BAD_BS_CONST'`, - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }]); }); }); @@ -209,7 +237,8 @@ describe('CompileErrorProcessor', () => { path: 'SimpleEntitlements.xml', range: bscUtil.createRange(0, 0, 0, 999), message: `Error parsing XML component`, - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }]); }); @@ -220,12 +249,14 @@ describe('CompileErrorProcessor', () => { path: 'SimpleEntitlements.xml', range: bscUtil.createRange(0, 0, 0, 999), message: `Error parsing XML component`, - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }, { path: 'Otherfile.xml', range: bscUtil.createRange(0, 0, 0, 999), message: `Error parsing XML component`, - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }]); }); @@ -236,7 +267,8 @@ describe('CompileErrorProcessor', () => { path: 'pkg:/components/SimpleEntitlements.xml', range: bscUtil.createRange(0, 0, 0, 999), message: `Error parsing XML component`, - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }]); }); @@ -247,12 +279,14 @@ describe('CompileErrorProcessor', () => { path: 'pkg:/components/SimpleEntitlements.xml', range: bscUtil.createRange(0, 0, 0, 999), message: `Error parsing XML component`, - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }, { path: 'pkg:/components/Otherfile.xml', range: bscUtil.createRange(0, 0, 0, 999), message: `Error parsing XML component`, - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }]); }); @@ -268,12 +302,14 @@ describe('CompileErrorProcessor', () => { path: 'SimpleEntitlements.xml', range: bscUtil.createRange(0, 0, 0, 999), message: `Error parsing XML component`, - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }, { path: 'Otherfile.xml', range: bscUtil.createRange(0, 0, 0, 999), message: `Error parsing XML component`, - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }]); }); }); @@ -292,12 +328,13 @@ describe('CompileErrorProcessor', () => { range: bscUtil.createRange(0, 0, 0, 999), message: 'Error loading file', path: 'pkg:/components/Scene/MainScene.GetConfigurationw.brs', - code: '&hb9' + code: '&hb9', + severity: DiagnosticSeverity.Error }]); }); describe('processUnhandledLines', () => { - async function runTest(lines: string[], expectedStatus: CompileStatus, expectedErrors?: BSDebugDiagnostic[]) { + async function runTest(lines: string | string[], expectedStatus: CompileStatus, expectedErrors?: BSDebugDiagnostic[]) { let compileErrors: BSDebugDiagnostic[]; let promise: Promise; if (expectedErrors) { @@ -309,9 +346,13 @@ describe('CompileErrorProcessor', () => { }); } - lines.forEach((line) => { - compiler.processUnhandledLines(line); - }); + if (typeof lines === 'string') { + compiler.processUnhandledLines(lines); + } else { + for (const line of lines) { + compiler.processUnhandledLines(line); + } + } if (expectedErrors) { //wait for the compiler-errors event @@ -321,6 +362,94 @@ describe('CompileErrorProcessor', () => { expect(compiler.status).to.eql(expectedStatus); } + it('handles the data in large chunks', async () => { + await runTest(dedent` + 10-05 18:03:33.677 [beacon.signal] |AppCompileInitiate --------> TimeBase(0 ms) + 10-05 18:03:33.679 [scrpt.cmpl] Compiling 'app', id 'dev' + 10-05 18:03:33.681 [scrpt.load.mkup] Loading markup dev 'app' + 10-05 18:03:33.681 [scrpt.unload.mkup] Unloading markup dev 'app' + 10-05 18:03:33.683 [scrpt.parse.mkup.time] Parsed markup dev 'app' in 1 milliseconds + + ------ Compiling dev 'app' ------ + + ================================================================= + Found 1 compile error + --- Syntax Error. (compile error &h02) in pkg:/components/MainScene.brs(3) + *** ERROR compiling MainScene: + + + ================================================================= + An error occurred while attempting to compile the application's components: + -------> Compilation Failed. + MainScene + `, CompileStatus.compileError, [{ + range: bscUtil.createRange(2, 0, 2, 999), + message: 'Syntax Error', + path: 'pkg:/components/MainScene.brs', + code: '&h02', + severity: DiagnosticSeverity.Error + }]); + }); + + it('emits component library errors after initial compile is complete', async () => { + await runTest(dedent` + 10-06 19:37:12.462 [beacon.signal] |AppLaunchInitiate ---------> TimeBase(0 ms) + 10-06 19:37:12.463 [beacon.signal] |AppCompileInitiate --------> TimeBase(0 ms) + 10-06 19:37:12.465 [scrpt.cmpl] Compiling 'app', id 'dev' + 10-06 19:37:12.466 [scrpt.load.mkup] Loading markup dev 'app' + 10-06 19:37:12.467 [scrpt.unload.mkup] Unloading markup dev 'app' + 10-06 19:37:12.468 [scrpt.parse.mkup.time] Parsed markup dev 'app' in 1 milliseconds + + ------ Compiling dev 'app' ------ + 10-06 19:37:12.471 [scrpt.ctx.cmpl.time] Compiled 'app', id 'dev' in 2 milliseconds (BCVer:0) + 10-06 19:37:12.471 [scrpt.proc.mkup.time] Processed markup dev 'app' in 0 milliseconds + 10-06 19:37:12.481 [beacon.signal] |AppCompileComplete --------> Duration(18 ms) + 10-06 19:37:12.498 [beacon.signal] |AppLaunchInitiate ---------> TimeBase(0 ms) + 10-06 19:37:12.508 [beacon.signal] |AppSplashInitiate ---------> TimeBase(9 ms) + 10-06 19:37:13.198 [beacon.signal] |AppSplashComplete ---------> Duration(690 ms) + 10-06 19:37:13.370 [beacon.signal] |AppLaunchInitiate ---------> TimeBase(0 ms) + 10-06 19:37:13.384 [scrpt.cmpl] Compiling 'app', id 'dev' + 10-06 19:37:13.391 [scrpt.load.mkup] Loading markup dev 'app' + 10-06 19:37:13.392 [scrpt.unload.mkup] Unloading markup dev 'app' + 10-06 19:37:13.394 [scrpt.parse.mkup.time] Parsed markup dev 'app' in 2 milliseconds + + ------ Compiling dev 'app' ------ + 10-06 19:37:13.399 [scrpt.ctx.cmpl.time] Compiled 'app', id 'dev' in 4 milliseconds (BCVer:0) + 10-06 19:37:13.399 [scrpt.proc.mkup.time] Processed markup dev 'app' in 0 milliseconds + 10-06 19:37:13.400 [beacon.signal] |AppCompileComplete --------> Duration(28 ms) + + ------ Running dev 'app' main ------ + 10-06 19:37:14.005 [scrpt.ctx.run.enter] UI: Entering 'app', id 'dev' + Complib loadStatus: loading + 10-06 19:37:14.212 [scrpt.cmpl] Compiling '', id 'RSG_BAAAAAJlSIgm' + 10-06 19:37:14.214 [scrpt.load.mkup] Loading markup RSG_BAAAAAJlSIgm '' + 10-06 19:37:14.215 [scrpt.unload.mkup] Unloading markup RSG_BAAAAAJlSIgm '' + 10-06 19:37:14.218 [scrpt.parse.mkup.time] Parsed markup RSG_BAAAAAJlSIgm '' in 4 milliseconds + + ================================================================= + Found 1 compile error + contained in ComponentLibrary package with uri + http://192.168.1.22:8080/complib.zip + --- Syntax Error. (compile error &h02) in pkg:/components/RedditViewer__lib0.brs(4) + *** ERROR compiling RedditViewer: + 10-06 19:37:14.505 [bs.ndk.proc.exit] plugin=dev pid=8755 status=signal retval=11 user requested=0 process name='SdkLauncher' exit code=EXIT_SYSTEM_KILL + 10-06 19:37:14.512 [beacon.signal] |AppExitInitiate -----------> TimeBase(2014 ms) + 10-06 19:37:14.514 [beacon.header] __________________________________________ + 10-06 19:37:14.514 [beacon.report] |AppLaunchInitiate ---------> TimeBase(0 ms), InstantOn + 10-06 19:37:14.515 [beacon.report] |AppSplashInitiate ---------> TimeBase(9 ms) + 10-06 19:37:14.515 [beacon.report] |AppSplashComplete ---------> Duration(690 ms) + 10-06 19:37:14.515 [beacon.report] |AppExitInitiate -----------> TimeBase(2014 ms) + 10-06 19:37:14.515 [beacon.report] |AppExitComplete -----------> Duration(2 ms) + 10-06 19:37:14.515 [beacon.footer] __________________________________________ + `, CompileStatus.compileError, [{ + range: bscUtil.createRange(3, 0, 3, 999), + message: 'Syntax Error', + path: 'pkg:/components/RedditViewer__lib0.brs', + code: '&h02', + severity: DiagnosticSeverity.Error + }]); + }); + it('detects No errors', async () => { let lines = [ `03-26 23:57:28.111 [beacon.signal] |AppLaunchInitiate ---------> TimeBase(0)`, @@ -368,7 +497,8 @@ describe('CompileErrorProcessor', () => { range: bscUtil.createRange(0, 0, 0, 999), message: 'Error loading file', path: 'pkg:/components/Scene/MainScene.GetConfigurationw.brs', - code: '&hb9' + code: '&hb9', + severity: DiagnosticSeverity.Error }]); }); @@ -398,22 +528,26 @@ describe('CompileErrorProcessor', () => { range: bscUtil.createRange(1, 0, 1, 999), message: 'Unexpected data found inside a element (first 10 characters are "aaa")', path: 'Foo.xml', - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }, { range: bscUtil.createRange(2, 0, 2, 999), message: 'Some unique error message', path: 'Foo.xml', - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }, { range: bscUtil.createRange(4, 0, 4, 999), message: 'message with Line 4 inside it', path: 'Foo.xml', - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }, { range: bscUtil.createRange(0, 0, 0, 999), message: 'Error parsing XML component', path: 'Foo.xml', - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }]); }); @@ -445,12 +579,14 @@ describe('CompileErrorProcessor', () => { range: bscUtil.createRange(2, 0, 2, 999), message: 'XML syntax error found ---> not well-formed (invalid token)', path: 'SampleScreen.xml', - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }, { range: bscUtil.createRange(0, 0, 0, 999), message: 'Error parsing XML component', path: 'SampleScreen.xml', - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error } ]); }); @@ -479,27 +615,32 @@ describe('CompileErrorProcessor', () => { range: bscUtil.createRange(595 - 1, 0, 595 - 1, 999), code: '&h02', message: 'Syntax Error', - path: 'pkg:/components/Services/Network/Parsers.brs' + path: 'pkg:/components/Services/Network/Parsers.brs', + severity: DiagnosticSeverity.Error }, { range: bscUtil.createRange(598 - 1, 0, 598 - 1, 999), code: '&h02', message: 'Syntax Error', - path: 'pkg:/components/Services/Network/Parsers.brs' + path: 'pkg:/components/Services/Network/Parsers.brs', + severity: DiagnosticSeverity.Error }, { range: bscUtil.createRange(732 - 1, 0, 732 - 1, 999), code: '&h02', message: 'Syntax Error', - path: 'pkg:/components/Services/Network/Parsers.brs' + path: 'pkg:/components/Services/Network/Parsers.brs', + severity: DiagnosticSeverity.Error }, { range: bscUtil.createRange(733 - 1, 0, 733 - 1, 999), code: '&h02', message: 'Syntax Error', - path: 'pkg:/components/Services/Network/Parsers.brs' + path: 'pkg:/components/Services/Network/Parsers.brs', + severity: DiagnosticSeverity.Error }, { range: bscUtil.createRange(734 - 1, 0, 734 - 1, 999), code: '&h02', message: 'Syntax Error', - path: 'pkg:/components/Services/Network/Parsers.brs' + path: 'pkg:/components/Services/Network/Parsers.brs', + severity: DiagnosticSeverity.Error } ]); }); @@ -539,32 +680,38 @@ describe('CompileErrorProcessor', () => { range: bscUtil.createRange(2, 0, 2, 999), message: 'XML syntax error found ---> not well-formed (invalid token)', path: 'SampleScreen.xml', - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }, { range: bscUtil.createRange(0, 0, 0, 999), message: 'Extends type does not exist: "ColoredButton"', path: 'pkg:/components/RedButton.xml', - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }, { range: bscUtil.createRange(8, 0, 8, 999), message: 'XML syntax error found ---> not well-formed (invalid token)', path: 'ChannelItemComponent.xml', - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }, { range: bscUtil.createRange(0, 0, 0, 999), message: 'Error parsing XML component', path: 'SampleScreen.xml', - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }, { range: bscUtil.createRange(0, 0, 0, 999), message: 'Error parsing XML component', path: 'ChannelItemComponent.xml', - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }, { range: bscUtil.createRange(0, 0, 0, 999), message: 'Error parsing XML component', path: 'RedButton.xml', - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error } ]); }); @@ -590,7 +737,8 @@ describe('CompileErrorProcessor', () => { code: '&h92', range: bscUtil.createRange(19 - 1, 0, 19 - 1, 999), message: `Invalid #If/#ElseIf expression ( not defined) 'BAD_BS_CONST'`, - path: 'Parsers.brs' + path: 'Parsers.brs', + severity: DiagnosticSeverity.Error } ]); }); @@ -614,7 +762,9 @@ describe('CompileErrorProcessor', () => { { range: bscUtil.createRange(0, 0, 0, 999), message: 'No manifest. Invalid package', - path: 'pkg:/manifest' + path: 'pkg:/manifest', + severity: DiagnosticSeverity.Error, + code: undefined } ]); }); diff --git a/src/CompileErrorProcessor.ts b/src/CompileErrorProcessor.ts index 0c4b23db..2d86ccdd 100644 --- a/src/CompileErrorProcessor.ts +++ b/src/CompileErrorProcessor.ts @@ -1,7 +1,7 @@ import { EventEmitter } from 'events'; import type { Diagnostic } from 'vscode-languageserver-protocol/node'; import { logger } from './logging'; -import { util as bscUtil } from 'brighterscript'; +import { DiagnosticSeverity, util as bscUtil } from 'brighterscript'; export class CompileErrorProcessor { @@ -28,52 +28,45 @@ export class CompileErrorProcessor { //emit these events on next tick, otherwise they will be processed immediately which could cause issues setTimeout(() => { //in rare cases, this event is fired after the debugger has closed, so make sure the event emitter still exists - if (this.emitter) { - this.emitter.emit(eventName, data); - } + this.emitter?.emit?.(eventName, data); }, 0); } public processUnhandledLines(responseText: string) { - if (this.status === CompileStatus.running) { - return; - } - - let newLines = responseText.split(/\r?\n/g); - switch (this.status) { - case CompileStatus.compiling: - case CompileStatus.compileError: - this.endCompilingLine = this.getEndCompilingLine(newLines); - if (this.endCompilingLine !== -1) { - this.logger.debug('[processUnhandledLines] entering state CompileStatus.running'); - this.status = CompileStatus.running; - this.resetCompileErrorTimer(false); - } else { - this.compilingLines = this.compilingLines.concat(newLines); - if (this.status === CompileStatus.compiling) { - //check to see if we've entered an error scenario - let hasError = /\berror\b/gi.test(responseText); - if (hasError) { - this.logger.debug('[processUnhandledLines] entering state CompileStatus.compileError'); - this.status = CompileStatus.compileError; + let lines = responseText.split(/\r?\n/g); + for (const line of lines) { + switch (this.status) { + case CompileStatus.compiling: + case CompileStatus.compileError: + if (this.isEndCompilingLine(line)) { + this.logger.debug('[processUnhandledLines] entering state CompileStatus.running'); + this.status = CompileStatus.running; + this.resetCompileErrorTimer(false); + } else { + this.compilingLines.push(line); + if (this.status === CompileStatus.compiling) { + //check to see if we've entered an error scenario + let hasError = /\berror\b/gi.test(line); + if (hasError) { + this.logger.debug('[processUnhandledLines] entering state CompileStatus.compileError'); + this.status = CompileStatus.compileError; + } + } + if (this.status === CompileStatus.compileError) { + //every input line while in error status will reset the stale timer, so we can wait for more errors to roll in. + this.resetCompileErrorTimer(true); } } - if (this.status === CompileStatus.compileError) { - //every input line while in error status will reset the stale timer, so we can wait for more errors to roll in. + break; + case CompileStatus.none: + case CompileStatus.running: + if (this.isStartingCompilingLine(line)) { + this.logger.debug('[processUnhandledLines] entering state CompileStatus.compiling'); + this.status = CompileStatus.compiling; this.resetCompileErrorTimer(true); } - } - break; - case CompileStatus.none: - this.startCompilingLine = this.getStartingCompilingLine(newLines); - this.compilingLines = this.compilingLines.concat(newLines); - if (this.startCompilingLine !== -1) { - this.logger.debug('[processUnhandledLines] entering state CompileStatus.compiling'); - newLines.splice(0, this.startCompilingLine); - this.status = CompileStatus.compiling; - this.resetCompileErrorTimer(true); - } - break; + break; + } } } @@ -88,7 +81,7 @@ export class CompileErrorProcessor { } public getErrors(lines: string[]) { - const result: BSDebugDiagnostic[] = []; + let diagnostics: BSDebugDiagnostic[] = []; //clone the lines so the parsers can manipulate them lines = [...lines]; while (lines.length > 0) { @@ -96,7 +89,7 @@ export class CompileErrorProcessor { const line = lines[0]; if (line) { - result.push( + diagnostics.push( ...[ this.processMultiLineErrors(lines), this.parseComponentDefinedInFileError(lines), @@ -111,10 +104,17 @@ export class CompileErrorProcessor { lines.shift(); } } - return result.filter(x => { - //throw out $livecompile errors (those are generated by REPL/eval code) + //throw out $livecompile errors (those are generated by REPL/eval code) + const result = diagnostics.filter(x => { return x.path && !x.path.toLowerCase().includes('$livecompile'); - }); + + //dedupe compile errors that have the same information + }).reduce((map, d) => { + map.set(`${d.path}:${d.range?.start.line}:${d.range?.start.character}-${d.message}-${d.severity}-${d.source}`, d); + return map; + }, new Map()); + + return [...result].map(x => x[1]); } /** @@ -137,7 +137,8 @@ export class CompileErrorProcessor { path: this.sanitizeCompilePath(filePath), range: bscUtil.createRange(0, 0, 0, 999), message: this.buildMessage(message), - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error })) .filter(x => !!x); } @@ -165,7 +166,8 @@ export class CompileErrorProcessor { path: this.sanitizeCompilePath(filePath), message: this.buildMessage(message, context), range: this.getRange(lineNumber), //lineNumber is 1-based - code: code + code: code, + severity: DiagnosticSeverity.Error }]; } } @@ -201,7 +203,8 @@ export class CompileErrorProcessor { path: filePath, range: this.getRange(lineNumber), //lineNumber is 1-based message: this.buildMessage(message), - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }); } else { //assume there are no more errors for this file @@ -235,7 +238,8 @@ export class CompileErrorProcessor { message: this.buildMessage(message), path: this.sanitizeCompilePath(filePath), range: this.getRange(), - code: undefined + code: undefined, + severity: DiagnosticSeverity.Error }]; } } @@ -257,7 +261,9 @@ export class CompileErrorProcessor { return [{ path: 'pkg:/manifest', range: bscUtil.createRange(0, 0, 0, 999), - message: this.buildMessage(message) + message: this.buildMessage(message), + code: undefined, + severity: DiagnosticSeverity.Error }]; } } @@ -304,7 +310,7 @@ export class CompileErrorProcessor { public resetCompileErrorTimer(isRunning): any { if (this.compileErrorTimer) { - clearInterval(this.compileErrorTimer); + clearTimeout(this.compileErrorTimer); this.compileErrorTimer = undefined; } @@ -323,29 +329,16 @@ export class CompileErrorProcessor { this.reportErrors(); } - private getStartingCompilingLine(lines: string[]): number { - let lastIndex = -1; - for (let i = 0; i < lines.length; i++) { - let line = lines[i]; - //if this line looks like the compiling line - if (/------\s+compiling.*------/i.exec(line)) { - lastIndex = i; - } - } - return lastIndex; + private isStartingCompilingLine(line: string): boolean { + //https://regex101.com/r/8W2wuZ/1 + // We need to start scanning for compile errors earlier than the ---compiling--- message, so look for the [scrpt.cmpl] message. + // keep the ---compiling--- as well, since it doesn't hurt to remain in compile mode + return /(------\s+compiling.*------)|(\[scrpt.cmpl]\s+compiling\s+'.*?'\s*,\s*id\s*'.*?')/i.test(line); } - private getEndCompilingLine(lines: string[]): number { - let lastIndex = -1; - for (let i = 0; i < lines.length; i++) { - let line = lines[i]; - // if this line looks like the compiling line - if (/------\s+Running.*------/i.exec(line)) { - lastIndex = i; - } - } - return lastIndex; - + private isEndCompilingLine(line: string): boolean { + // if this line looks like the compiling line + return /------\s+Running.*------/i.test(line); } /** @@ -377,6 +370,10 @@ export interface BSDebugDiagnostic extends Diagnostic { * main app. */ componentLibraryName?: string; + /** + * The diagnostic's severity. + */ + severity: DiagnosticSeverity; } export enum CompileStatus { diff --git a/src/DeviceInfo.ts b/src/DeviceInfo.ts index b52f90af..967d02cc 100644 --- a/src/DeviceInfo.ts +++ b/src/DeviceInfo.ts @@ -66,6 +66,7 @@ export interface DeviceInfo { 'trc-channel-version'?: string; 'davinci-version'?: string; 'av-sync-calibration-enabled'?: number; + 'brightscript-debugger-version'?: string; // Anything new they might add that we do not know about [key: string]: any; } diff --git a/src/adapters/DebugProtocolAdapter.spec.ts b/src/adapters/DebugProtocolAdapter.spec.ts index 1cad8cd3..71952cbc 100644 --- a/src/adapters/DebugProtocolAdapter.spec.ts +++ b/src/adapters/DebugProtocolAdapter.spec.ts @@ -1,84 +1,556 @@ +/* eslint-disable prefer-arrow-callback */ import { expect } from 'chai'; -import { Debugger } from '../debugProtocol/Debugger'; -import { DebugProtocolAdapter } from './DebugProtocolAdapter'; +import type { DebugProtocolClient } from '../debugProtocol/client/DebugProtocolClient'; +import { DebugProtocolAdapter, KeyType } from './DebugProtocolAdapter'; import { createSandbox } from 'sinon'; -import type { VariableInfo } from '../debugProtocol/responses'; -import { VariableResponse } from '../debugProtocol/responses'; -import { ERROR_CODES } from './../debugProtocol/Constants'; +import { VariableType, VariablesResponse } from '../debugProtocol/events/responses/VariablesResponse'; +import { DebugProtocolServer } from '../debugProtocol/server/DebugProtocolServer'; +import { defer, util } from '../util'; +import { standardizePath as s } from 'brighterscript'; +import { DebugProtocolServerTestPlugin } from '../debugProtocol/DebugProtocolServerTestPlugin.spec'; +import { AllThreadsStoppedUpdate } from '../debugProtocol/events/updates/AllThreadsStoppedUpdate'; +import { ErrorCode, StopReason } from '../debugProtocol/Constants'; +import { ThreadsResponse } from '../debugProtocol/events/responses/ThreadsResponse'; +import { StackTraceV3Response } from '../debugProtocol/events/responses/StackTraceV3Response'; +import { AddBreakpointsResponse } from '../debugProtocol/events/responses/AddBreakpointsResponse'; +import { BreakpointManager } from '../managers/BreakpointManager'; +import { SourceMapManager } from '../managers/SourceMapManager'; +import { LocationManager } from '../managers/LocationManager'; +import { Project, ProjectManager } from '../managers/ProjectManager'; +import { AddBreakpointsRequest } from '../debugProtocol/events/requests/AddBreakpointsRequest'; +import { AddConditionalBreakpointsRequest } from '../debugProtocol/events/requests/AddConditionalBreakpointsRequest'; +import { AddConditionalBreakpointsResponse } from '../debugProtocol/events/responses/AddConditionalBreakpointsResponse'; +import { RemoveBreakpointsResponse } from '../debugProtocol/events/responses/RemoveBreakpointsResponse'; +import { BreakpointVerifiedUpdate } from '../debugProtocol/events/updates/BreakpointVerifiedUpdate'; +import { RemoveBreakpointsRequest } from '../debugProtocol/events/requests/RemoveBreakpointsRequest'; +import type { AfterSendRequestEvent } from '../debugProtocol/client/DebugProtocolClientPlugin'; +import { GenericV3Response } from '../debugProtocol/events/responses/GenericV3Response'; import { RendezvousTracker } from '../RendezvousTracker'; const sinon = createSandbox(); -describe('DebugProtocolAdapter', () => { +let cwd = s`${process.cwd()}`; +let tmpDir = s`${cwd}/.tmp`; +let rootDir = s`${tmpDir}/rootDir`; +const outDir = s`${tmpDir}/out`; +/** + * A path to main.brs + */ +const srcPath = `${rootDir}/source/main.brs`; + +describe('DebugProtocolAdapter', function() { + //allow these tests to run for longer since there's more IO overhead due to the socket logic + this.timeout(3000); let adapter: DebugProtocolAdapter; - let socketDebugger: Debugger; + let server: DebugProtocolServer; + let client: DebugProtocolClient; + let plugin: DebugProtocolServerTestPlugin; + let breakpointManager: BreakpointManager; + let projectManager: ProjectManager; let deviceInfo = { 'software-version': '11.5.0', 'host': '192.168.1.5', 'remotePort': 8060 }; let rendezvousTracker = new RendezvousTracker(deviceInfo); - beforeEach(() => { - - adapter = new DebugProtocolAdapter( - { - host: '127.0.0.1' - }, - undefined, - undefined, - rendezvousTracker + + beforeEach(async () => { + sinon.stub(console, 'log').callsFake((...args) => { }); + const options = { + controlPort: undefined as number, + host: '127.0.0.1' + }; + const sourcemapManager = new SourceMapManager(); + const locationManager = new LocationManager(sourcemapManager); + const rendezvousTracker = new RendezvousTracker({}); + breakpointManager = new BreakpointManager(sourcemapManager, locationManager); + projectManager = new ProjectManager(breakpointManager, locationManager); + projectManager.mainProject = new Project({ + rootDir: rootDir, + files: [], + outDir: outDir + }); + adapter = new DebugProtocolAdapter(options, projectManager, breakpointManager, rendezvousTracker, deviceInfo); + + if (!options.controlPort) { + options.controlPort = await util.getPort(); + } + server = new DebugProtocolServer(options); + plugin = server.plugins.add(new DebugProtocolServerTestPlugin()); + await server.start(); + }); + + afterEach(async () => { + sinon.restore(); + client?.destroy(true); + //shut down and destroy the server after each test + await server?.stop(); + await util.sleep(10); + }); + + /** + * Handles the initial connection and the "stop at first byte code" flow + */ + async function initialize() { + await adapter.connect(); + client = adapter['socketDebugger']; + client['options'].shutdownTimeout = 100; + client['options'].exitChannelTimeout = 100; + //disable logging for tests because they clutter the test output + client['logger'].logLevel = 'off'; + await Promise.all([ + adapter.once('suspend'), + plugin.server.sendUpdate( + AllThreadsStoppedUpdate.fromJson({ + stopReason: StopReason.Break, + stopReasonDetail: 'initial stop', + threadIndex: 0 + }) + ) + ]); + + //the stackTrace request first sends a threads request + plugin.pushResponse( + ThreadsResponse.fromJson({ + requestId: undefined, + threads: [{ + filePath: 'pkg:/source/main.brs', + lineNumber: 12, + functionName: 'main', + isPrimary: true, + codeSnippet: '', + stopReason: StopReason.Break, + stopReasonDetail: 'because' + }] + }) ); - socketDebugger = new Debugger(undefined); - adapter['socketDebugger'] = socketDebugger; + //then it sends the stacktrace request + plugin.pushResponse( + StackTraceV3Response.fromJson({ + requestId: undefined, + entries: [{ + filePath: 'pkg:/source/main.brs', + functionName: 'main', + lineNumber: 12 + }] + }) + ); + //load stack frames + await adapter.getStackTrace(0); + } + + describe('getStackTrace', () => { + it('recovers when there are no stack frames', async () => { + await initialize(); + //should not throw exception + expect( + await adapter.getStackTrace(-1) + ).to.eql([]); + }); }); - describe('getVariable', () => { - let response: VariableResponse; - let variables: Partial[]; - - beforeEach(() => { - response = new VariableResponse(Buffer.alloc(5)); - response.errorCode = ERROR_CODES.OK; - variables = []; - sinon.stub(adapter as any, 'getStackFrameById').returns({}); - sinon.stub(socketDebugger, 'getVariables').callsFake(() => { - response.variables = variables as any; - return Promise.resolve(response); + describe('syncBreakpoints', () => { + it('retries at next sync() to delete breakpoints if first request failed', async () => { + await initialize(); + //disable auto breakpoint verification + client.protocolVersion = '3.2.0'; + + //add a single breakpoint first and do a diff to lock in the diff + const bp2 = breakpointManager.setBreakpoint(srcPath, { line: 2 }); + + await breakpointManager.getDiff(projectManager.getAllProjects()); + + //add breakpoints + const [bp1, bp3] = breakpointManager.replaceBreakpoints(srcPath, [ + { line: 1 }, + { line: 3 } + ]); + + //sync the breakpoints so they get added + plugin.pushResponse(AddBreakpointsResponse.fromJson({ + breakpoints: [{ + id: 8, + errorCode: ErrorCode.OK, + ignoreCount: 0 + }, { + id: 9, + errorCode: ErrorCode.OK, + ignoreCount: 0 + }], + requestId: 1 + })); + + //sync the breakpoints. this request will fail, so try deleting the breakpoints again later + await adapter.syncBreakpoints(); + + //now try to delete the breakpoints + breakpointManager.deleteBreakpoints([bp1, bp3]); + + //complete request failure because debugger not stopped + plugin.pushResponse(GenericV3Response.fromJson({ + errorCode: ErrorCode.NOT_STOPPED, + requestId: 1 + })); + + //sync the breakpoints again. ask to delete the breakpoints, but it fails. + await adapter.syncBreakpoints(); + + expect( + [...breakpointManager.failedDeletions.values()].map(x => x.deviceId) + ).to.eql([8, 9]); + + plugin.pushResponse(RemoveBreakpointsResponse.fromJson({ + breakpoints: [{ + id: 8, + errorCode: ErrorCode.OK, + ignoreCount: 0 + }, { + id: 9, + errorCode: ErrorCode.OK, + ignoreCount: 0 + }], + requestId: 1 + })); + + await adapter.syncBreakpoints(); + expect(plugin.getLatestRequest().data.breakpointIds).to.eql([8, 9]); + }); + + it('removes any newly-added breakpoints that have errors', async () => { + await initialize(); + + const [bp1, bp2] = breakpointManager.replaceBreakpoints(srcPath, [ + { line: 1 }, + { line: 2 } + ]); + + plugin.pushResponse(AddBreakpointsResponse.fromJson({ + breakpoints: [{ + id: 3, + errorCode: ErrorCode.OK, + ignoreCount: 0 + }, { + id: 4, + errorCode: ErrorCode.INVALID_ARGS, + ignoreCount: 0 + }], + requestId: 1 + })); + + //sync breakpoints + await adapter.syncBreakpoints(); + + //the bad breakpoint (id=2) should now be removed + expect(breakpointManager.getBreakpoints([bp1, bp2])).to.eql([bp1]); + }); + + it('only allows one to run at a time', async () => { + let concurrentCount = 0; + let maxConcurrentCount = 0; + + sinon.stub(adapter, '_syncBreakpoints').callsFake(async () => { + console.log('_syncBreakpoints'); + concurrentCount++; + maxConcurrentCount = Math.max(0, concurrentCount); + //several nextticks here to give other promises a chance to run + await util.sleep(0); + maxConcurrentCount = Math.max(0, concurrentCount); + await util.sleep(0); + maxConcurrentCount = Math.max(0, concurrentCount); + await util.sleep(0); + maxConcurrentCount = Math.max(0, concurrentCount); + await util.sleep(0); + maxConcurrentCount = Math.max(0, concurrentCount); + concurrentCount--; + }); + + await Promise.all([ + adapter.syncBreakpoints(), + adapter.syncBreakpoints(), + adapter.syncBreakpoints(), + adapter.syncBreakpoints(), + adapter.syncBreakpoints(), + adapter.syncBreakpoints(), + adapter.syncBreakpoints(), + adapter.syncBreakpoints(), + adapter.syncBreakpoints(), + adapter.syncBreakpoints(), + adapter.syncBreakpoints() + ]); + expect(maxConcurrentCount).to.eql(1); + }); + + it('removes "added" breakpoints that show up after a breakpoint was already removed', async () => { + const bpId = 123; + const bpLine = 12; + await initialize(); + + //force the client to expect the device to verify breakpoints (instead of auto-verifying them as soon as seen) + client.protocolVersion = '3.2.0'; + + breakpointManager.setBreakpoint(srcPath, { + line: bpLine + }); + + let bpResponseDeferred = defer(); + + //once the breakpoint arrives at the server + let bpAtServerPromise = new Promise((resolve) => { + let handled = false; + const tempPlugin = server.plugins.add({ + provideResponse: (event) => { + if (!handled && event.request instanceof AddBreakpointsRequest) { + handled = true; + //resolve the outer promise + resolve(); + //return a deferred promise for us to flush later + return bpResponseDeferred.promise; + } + } + }); + }); + + plugin.pushResponse( + AddBreakpointsResponse.fromJson({ + requestId: 1, + breakpoints: [{ + id: bpId, + errorCode: ErrorCode.OK, + ignoreCount: 0 + }] + }) + ); + //sync the breakpoints to mark this one as "sent to device" + void adapter.syncBreakpoints(); + //wait for the request to arrive at the server (it will be stuck pending until we resolve the bpResponseDeferred) + await bpAtServerPromise; + + //delete the breakpoint (before we ever got the deviceId from the server) + breakpointManager.replaceBreakpoints(srcPath, []); + + //run another breakpoint diff to simulate the breakpoint being deleted before the device responded with the device IDs + await breakpointManager.getDiff(projectManager.getAllProjects()); + + //sync the breakpoints again, forcing the bp to be fully deleted + let syncPromise = adapter.syncBreakpoints(); + //since the breakpoints were deleted before getting deviceIDs, there should be no request sent + bpResponseDeferred.resolve(); + //wait for the second sync to finish + await syncPromise; + + //response for the "remove breakpoints" request triggered later + plugin.pushResponse( + RemoveBreakpointsResponse.fromJson({ + requestId: 1, + breakpoints: [{ + id: bpId, + errorCode: ErrorCode.OK, + ignoreCount: 0 + }] + }) + ); + + //listen for the next sent RemoveBreakpointsRequest + const sentRequestPromise = client.plugins.onceIf>('afterSendRequest', (event) => { + return event.request instanceof RemoveBreakpointsRequest; + }, 0); + + //now push the "bp verified" event + //the client should recognize that these breakpoints aren't avaiable client-side, and ask the server to delete them + await server.sendUpdate( + BreakpointVerifiedUpdate.fromJson({ + breakpoints: [{ + id: bpId + }] + }) + ); + + //wait for the request to be sent + expect( + (await sentRequestPromise).request?.data.breakpointIds + ).to.eql([bpId]); + }); + + it('excludes non-numeric breakpoint IDs', async () => { + await initialize(); + + const breakpoint = adapter['breakpointManager'].setBreakpoint(`${rootDir}/source/main.brs`, { + line: 12 + }); + plugin.pushResponse( + AddBreakpointsResponse.fromJson({ + breakpoints: [{ id: 10 } as any], + requestId: 1 + }) + ); + //sync the breakpoints to mark this one as "sent to device" + await adapter.syncBreakpoints(); + + // //replace the breakpoints before they were verified + // adapter['breakpointManager'].replaceBreakpoints(`${rootDir}/source/main.brs`, []); + // breakpoint.deviceId = undefined; + + // //sync the breakpoints again. Since the breakpoint doesn't have an ID, we shouldn't send any request + // await adapter.syncBreakpoints(); + + // expect(plugin.latestRequest?.constructor.name).not.to.eql(RemoveBreakpointsResponse.name); + }); + + it('skips sending AddBreakpoints and AddConditionalBreakpoints command when there are no breakpoints', async () => { + await initialize(); + + await adapter.syncBreakpoints(); + const reqs = [ + plugin.getRequest(-2)?.constructor.name, + plugin.getRequest(-1)?.constructor.name + ]; + expect(reqs).not.to.include(AddBreakpointsRequest.name); + expect(reqs).not.to.include(AddConditionalBreakpointsRequest.name); + }); + + it('skips sending AddConditionalBreakpoints command when there were only standard breakpoints', async () => { + await initialize(); + + adapter['breakpointManager'].setBreakpoint(`${rootDir}/source/main.brs`, { + line: 12 }); - socketDebugger['stopped'] = true; + + //let the "add" request go through + plugin.pushResponse( + AddConditionalBreakpointsResponse.fromJson({ + breakpoints: [], + requestId: 1 + }) + ); + await adapter.syncBreakpoints(); + const reqs = [ + plugin.getRequest(-2)?.constructor.name, + plugin.getRequest(-1)?.constructor.name + ]; + expect(reqs).to.include(AddBreakpointsRequest.name); + expect(reqs).not.to.include(AddConditionalBreakpointsRequest.name); }); + it('skips sending AddBreakpoints command when there only conditional breakpoints', async () => { + await initialize(); + + adapter['breakpointManager'].setBreakpoint(`${rootDir}/source/main.brs`, { + line: 12, + condition: 'true' + }); + + //let the "add" request go through + plugin.pushResponse( + AddBreakpointsResponse.fromJson({ + breakpoints: [], + requestId: 1 + }) + ); + await adapter.syncBreakpoints(); + const reqs = [ + plugin.getRequest(-2)?.constructor.name, + plugin.getRequest(-1)?.constructor.name + ]; + expect(reqs).not.to.include(AddBreakpointsRequest.name); + expect(reqs).to.include(AddConditionalBreakpointsRequest.name); + }); + }); + + describe('getVariable', () => { it('works for local vars', async () => { - variables.push( - { name: 'm' }, - { name: 'person' }, - { name: 'age' } + await initialize(); + + plugin.pushResponse( + VariablesResponse.fromJson({ + requestId: undefined, + variables: [ + { + isConst: false, + isContainer: true, + refCount: 1, + type: VariableType.AssociativeArray, + value: undefined, + childCount: 4, + keyType: VariableType.String, + name: 'm' + }, + { + isConst: false, + isContainer: false, + refCount: 1, + type: VariableType.String, + value: '1.0.0', + name: 'apiVersion' + } + ] + }) ); - const vars = await adapter.getVariable('', 1, true); + const vars = await adapter.getLocalVariables(1); expect( vars?.children.map(x => x.evaluateName) ).to.eql([ 'm', - 'person', - 'age' + 'apiVersion' ]); }); it('works for object properties', async () => { - variables.push( - { isContainer: true, elementCount: 2, isChildKey: false, variableType: 'AA' }, - { name: 'name', isChildKey: true }, - { name: 'age', isChildKey: true } - ); + await initialize(); + + //load the stack trace which is required for variable requests to work + const frames = await adapter.getStackTrace(0); + const frameId = frames[0].frameId; - const vars = await adapter.getVariable('person', 1, true); + plugin.pushResponse( + VariablesResponse.fromJson({ + requestId: undefined, + variables: [ + { + isConst: false, + isContainer: true, + refCount: 1, + type: VariableType.AssociativeArray, + value: undefined, + keyType: VariableType.String, + children: [{ + isConst: false, + isContainer: false, + refCount: 1, + type: VariableType.String, + name: 'name', + value: 'bob' + }, { + isConst: false, + isContainer: false, + refCount: 1, + type: VariableType.Integer, + name: 'age', + value: 12 + }] + } + ] + }) + ); + const container = await adapter.getVariable('person', frameId); expect( - vars?.children.map(x => x.evaluateName) + container?.children.map(x => x.evaluateName) ).to.eql([ 'person["name"]', 'person["age"]' ]); - }); + //the top level object should be an AA + expect(container.type).to.eql(VariableType.AssociativeArray); + expect(container.keyType).to.eql(KeyType.string); + expect(container.elementCount).to.eql(2); + //the children should NOT look like objects + expect(container.children[0].keyType).not.to.exist; + expect(container.children[0].elementCount).not.to.exist; + expect(container.children[0].children).to.eql([]); + + expect(container.children[1].keyType).not.to.exist; + expect(container.children[1].elementCount).not.to.exist; + expect(container.children[1].children).to.eql([]); + }); }); }); diff --git a/src/adapters/DebugProtocolAdapter.ts b/src/adapters/DebugProtocolAdapter.ts index 92bef3b9..033c17f9 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -1,22 +1,25 @@ -import type { BreakpointSpec, ConstructorOptions, ProtocolVersionDetails } from '../debugProtocol/Debugger'; -import { Debugger } from '../debugProtocol/Debugger'; import * as EventEmitter from 'events'; import { Socket } from 'net'; +import { DiagnosticSeverity, util as bscUtil } from 'brighterscript'; import type { BSDebugDiagnostic } from '../CompileErrorProcessor'; import { CompileErrorProcessor } from '../CompileErrorProcessor'; import type { RendezvousHistory, RendezvousTracker } from '../RendezvousTracker'; import type { ChanperfData } from '../ChanperfTracker'; import { ChanperfTracker } from '../ChanperfTracker'; import type { SourceLocation } from '../managers/LocationManager'; -import { ERROR_CODES, PROTOCOL_ERROR_CODES, STOP_REASONS } from '../debugProtocol/Constants'; +import { ErrorCode, PROTOCOL_ERROR_CODES, UpdateType } from '../debugProtocol/Constants'; import { defer, util } from '../util'; import { logger } from '../logging'; import * as semver from 'semver'; import type { AdapterOptions, HighLevelType, RokuAdapterEvaluateResponse } from '../interfaces'; import type { BreakpointManager } from '../managers/BreakpointManager'; import type { ProjectManager } from '../managers/ProjectManager'; -import { ActionQueue } from '../managers/ActionQueue'; -import type { VerifiedBreakpointsData } from '../debugProtocol/responses/BreakpointVerifiedUpdateResponse'; +import type { BreakpointsVerifiedEvent, ConstructorOptions, ProtocolVersionDetails } from '../debugProtocol/client/DebugProtocolClient'; +import { DebugProtocolClient } from '../debugProtocol/client/DebugProtocolClient'; +import type { Variable } from '../debugProtocol/events/responses/VariablesResponse'; +import { VariableType } from '../debugProtocol/events/responses/VariablesResponse'; +import type { TelnetAdapter } from './TelnetAdapter'; +import type { DeviceInfo } from '../DeviceInfo'; /** * A class that connects to a Roku device over telnet debugger port and provides a standardized way of interacting with it. @@ -26,7 +29,8 @@ export class DebugProtocolAdapter { private options: AdapterOptions & ConstructorOptions, private projectManager: ProjectManager, private breakpointManager: BreakpointManager, - private rendezvousTracker: RendezvousTracker + private rendezvousTracker: RendezvousTracker, + private deviceInfo: DeviceInfo ) { util.normalizeAdapterOptions(this.options); this.emitter = new EventEmitter(); @@ -40,7 +44,7 @@ export class DebugProtocolAdapter { }); } - private logger = logger.createLogger(`[${DebugProtocolAdapter.name}]`); + private logger = logger.createLogger(`[padapter]`); /** * Indicates whether the adapter has successfully established a connection with the device @@ -57,7 +61,7 @@ export class DebugProtocolAdapter { private compileErrorProcessor: CompileErrorProcessor; private emitter: EventEmitter; private chanperfTracker: ChanperfTracker; - private socketDebugger: Debugger; + private socketDebugger: DebugProtocolClient; private nextFrameId = 1; private stackFramesCache: Record = {}; @@ -72,12 +76,38 @@ export class DebugProtocolAdapter { public readonly supportsMultipleRuns = false; + /** + * Subscribe to an event exactly once + * @param eventName + */ + public once(eventName: 'cannot-continue'): Promise; + public once(eventname: 'chanperf'): Promise; + public once(eventName: 'close'): Promise; + public once(eventName: 'app-exit'): Promise; + public once(eventName: 'diagnostics'): Promise; + public once(eventName: 'connected'): Promise; + public once(eventname: 'console-output'): Promise; // TODO: might be able to remove this at some point + public once(eventname: 'protocol-version'): Promise; + public once(eventname: 'rendezvous'): Promise; + public once(eventName: 'runtime-error'): Promise; + public once(eventName: 'suspend'): Promise; + public once(eventName: 'start'): Promise; + public once(eventname: 'unhandled-console-output'): Promise; + public once(eventName: string) { + return new Promise((resolve) => { + const disconnect = this.on(eventName as Parameters[0], (...args) => { + disconnect(); + resolve(...args); + }); + }); + } + /** * Subscribe to various events * @param eventName * @param handler */ - public on(eventName: 'breakpoints-verified', handler: (data: VerifiedBreakpointsData) => void); + public on(eventName: 'breakpoints-verified', handler: (event: BreakpointsVerifiedEvent) => void); public on(eventName: 'cannot-continue', handler: () => void); public on(eventname: 'chanperf', handler: (output: ChanperfData) => void); public on(eventName: 'close', handler: () => void); @@ -91,16 +121,14 @@ export class DebugProtocolAdapter { public on(eventName: 'start', handler: () => void); public on(eventname: 'unhandled-console-output', handler: (output: string) => void); public on(eventName: string, handler: (payload: any) => void) { - this.emitter.on(eventName, handler); + this.emitter?.on(eventName, handler); return () => { - if (this.emitter !== undefined) { - this.emitter.removeListener(eventName, handler); - } + this.emitter?.removeListener(eventName, handler); }; } private emit(eventName: 'suspend'); - private emit(eventName: 'breakpoints-verified', data: VerifiedBreakpointsData); + private emit(eventName: 'breakpoints-verified', event: BreakpointsVerifiedEvent); private emit(eventName: 'diagnostics', data: BSDebugDiagnostic[]); private emit(eventName: 'app-exit' | 'cannot-continue' | 'chanperf' | 'close' | 'connected' | 'console-output' | 'protocol-version' | 'rendezvous' | 'runtime-error' | 'start' | 'unhandled-console-output', data?); private emit(eventName: string, data?) { @@ -183,7 +211,7 @@ export class DebugProtocolAdapter { */ public async connect() { let deferred = defer(); - this.socketDebugger = new Debugger(this.options); + this.socketDebugger = new DebugProtocolClient(this.options); try { // Emit IO from the debugger. // eslint-disable-next-line @typescript-eslint/no-misused-promises @@ -237,7 +265,7 @@ export class DebugProtocolAdapter { console.debug('hasRuntimeError!!', data); this.emit('runtime-error', { message: data.data.stopReasonDetail, - errorCode: STOP_REASONS[data.data.stopReason] + errorCode: data.data.stopReason }); }); @@ -247,9 +275,19 @@ export class DebugProtocolAdapter { //handle when the device verifies breakpoints this.socketDebugger.on('breakpoints-verified', (event) => { + let unverifiableDeviceIds = [] as number[]; + //mark the breakpoints as verified for (let breakpoint of event?.breakpoints ?? []) { - this.breakpointManager.verifyBreakpoint(breakpoint.breakpointId, true); + const success = this.breakpointManager.verifyBreakpoint(breakpoint.id, true); + if (!success) { + unverifiableDeviceIds.push(breakpoint.id); + } + } + //if there were any unsuccessful breakpoint verifications, we need to ask the device to delete those breakpoints as they've gone missing on our side + if (unverifiableDeviceIds.length > 0) { + this.logger.warn('Could not find breakpoints to verify. Removing from device:', { deviceBreakpointIds: unverifiableDeviceIds }); + void this.socketDebugger.removeBreakpoints(unverifiableDeviceIds); } this.emit('breakpoints-verified', event); }); @@ -263,6 +301,18 @@ export class DebugProtocolAdapter { this.compileClient = undefined; } + this.socketDebugger.on('compile-error', (update) => { + let diagnostics: BSDebugDiagnostic[] = []; + diagnostics.push({ + path: update.data.filePath, + range: bscUtil.createRange(update.data.lineNumber - 1, 0, update.data.lineNumber - 1, 999), + message: update.data.errorMessage, + severity: DiagnosticSeverity.Error, + code: undefined + }); + this.emit('diagnostics', diagnostics); + }); + this.logger.log(`Connected to device`, { host: this.options.host, connected: this.connected }); this.emit('connected', this.connected); @@ -281,8 +331,19 @@ export class DebugProtocolAdapter { }, 200); } + /** + * Determines if the current version of the debug protocol supports emitting compile error updates. + */ + public get supportsCompileErrorReporting() { + return semver.satisfies(this.deviceInfo['brightscript-debugger-version'], '>=3.1.0'); + } + public async watchCompileOutput() { let deferred = defer(); + //If the debugProtocol supports compile error updates, don't scrape telnet logs for compile errors + if (this.supportsCompileErrorReporting) { + return deferred.resolve(); + } try { this.compileClient = new Socket(); this.compileErrorProcessor.on('diagnostics', (errors) => { @@ -296,7 +357,7 @@ export class DebugProtocolAdapter { }); this.logger.info('Connecting via telnet to gather compile info', { host: this.options.host, port: this.options.brightScriptConsolePort }); this.compileClient.connect(this.options.brightScriptConsolePort, this.options.host, () => { - this.logger.log(`Connected via telnet to gather compile info`, { host: this.options.host, port: this.options.brightScriptConsolePort }); + this.logger.log(`CONNECTED via telnet to gather compile info`, { host: this.options.host, port: this.options.brightScriptConsolePort }); }); this.logger.debug('Waiting for the compile client to settle'); @@ -394,14 +455,19 @@ export class DebugProtocolAdapter { const response = await this.socketDebugger.executeCommand(command, stackFrame.frameIndex, stackFrame.threadIndex); this.logger.info('evaluate response', { command, response }); - if (response.executeSuccess) { + if (response.data.executeSuccess) { return { message: undefined, type: 'message' }; } else { + const messages = [ + ...response?.data?.compileErrors ?? [], + ...response?.data?.runtimeErrors ?? [], + ...response?.data?.otherErrors ?? [] + ]; return { - message: response.compileErrors.messages[0] ?? response.runtimeErrors.messages[0] ?? response.otherErrors.messages[0] ?? 'Unknown error executing command', + message: messages[0] ?? 'Unknown error executing command', type: 'error' }; } @@ -413,22 +479,22 @@ export class DebugProtocolAdapter { } } - public async getStackTrace(threadId: number = this.socketDebugger.primaryThread) { + public async getStackTrace(threadIndex: number = this.socketDebugger.primaryThread) { if (!this.isAtDebuggerPrompt) { throw new Error('Cannot get stack trace: debugger is not paused'); } - return this.resolve(`stack trace for thread ${threadId}`, async () => { - let thread = await this.getThreadByThreadId(threadId); + return this.resolve(`stack trace for thread ${threadIndex}`, async () => { + let thread = await this.getThreadByThreadId(threadIndex); let frames: StackFrame[] = []; - let stackTraceData = await this.socketDebugger.stackTrace(threadId); - for (let i = 0; i < stackTraceData.stackSize; i++) { - let frameData = stackTraceData.entries[i]; + let stackTraceData = await this.socketDebugger.getStackTrace(threadIndex); + for (let i = 0; i < stackTraceData?.data?.entries?.length ?? 0; i++) { + let frameData = stackTraceData.data.entries[i]; let stackFrame: StackFrame = { frameId: this.nextFrameId++, // frame index is the reverse of the returned order. - frameIndex: stackTraceData.stackSize - i - 1, - threadIndex: threadId, - filePath: frameData.fileName, + frameIndex: stackTraceData.data.entries.length - i - 1, + threadIndex: threadIndex, + filePath: frameData.filePath, lineNumber: frameData.lineNumber, // eslint-disable-next-line no-nested-ternary functionIdentifier: this.cleanUpFunctionName(i === 0 ? (frameData.functionName) ? frameData.functionName : thread.functionName : frameData.functionName) @@ -455,11 +521,12 @@ export class DebugProtocolAdapter { } /** - * Given an expression, evaluate that statement ON the roku - * @param expression + * Get info about the specified variable. + * @param expression the expression for the specified variable (i.e. `m`, `someVar.value`, `arr[1][2].three`). If empty string/undefined is specified, all local variables are retrieved instead */ - public async getVariable(expression: string, frameId: number, withChildren = true) { - const logger = this.logger.createLogger(' getVariable'); + private async getVariablesResponse(expression: string, frameId: number) { + const isScopesRequest = expression === ''; + const logger = this.logger.createLogger('[getVariable]'); logger.info('begin', { expression }); if (!this.isAtDebuggerPrompt) { throw new Error('Cannot resolve variable: debugger is not paused'); @@ -470,7 +537,7 @@ export class DebugProtocolAdapter { throw new Error('Cannot request variable without a corresponding frame'); } - logger.log(`Expression:`, expression); + logger.info(`Expression:`, JSON.stringify(expression)); let variablePath = expression === '' ? [] : util.getVariablePath(expression); // Temporary workaround related to casing issues over the protocol @@ -478,85 +545,125 @@ export class DebugProtocolAdapter { variablePath[0] = variablePath[0].toLowerCase(); } - let response = await this.socketDebugger.getVariables(variablePath, withChildren, frame.frameIndex, frame.threadIndex); + let response = await this.socketDebugger.getVariables(variablePath, frame.frameIndex, frame.threadIndex); - if (this.enableVariablesLowerCaseRetry && response.errorCode !== ERROR_CODES.OK) { + if (this.enableVariablesLowerCaseRetry && response.data.errorCode !== ErrorCode.OK) { // Temporary workaround related to casing issues over the protocol logger.log(`Retrying expression as lower case:`, expression); variablePath = expression === '' ? [] : util.getVariablePath(expression?.toLowerCase()); - response = await this.socketDebugger.getVariables(variablePath, withChildren, frame.frameIndex, frame.threadIndex); + response = await this.socketDebugger.getVariables(variablePath, frame.frameIndex, frame.threadIndex); } + return response; + } + /** + * Get the variable for the specified expression. + */ + public async getVariable(expression: string, frameId: number) { + const response = await this.getVariablesResponse(expression, frameId); + + if (Array.isArray(response?.data?.variables)) { + const container = this.createEvaluateContainer( + response.data.variables[0], + //the name of the top container is the expression itself + expression, + //this is the top-level container, so there are no parent keys to this entry + undefined + ); + return container; + } + } - if (response.errorCode === ERROR_CODES.OK) { - let mainContainer: EvaluateContainer; - let children: EvaluateContainer[] = []; - let firstHandled = false; - for (let variable of response.variables) { - let value; - let variableType = variable.variableType; - if (variable.value === null) { - value = 'roInvalid'; - } else if (variableType === 'String') { - value = `\"${variable.value}\"`; - } else { - value = variable.value; - } + /** + * Get the list of local variables + */ + public async getLocalVariables(frameId: number) { + const response = await this.getVariablesResponse('', frameId); + + if (response?.data?.errorCode === ErrorCode.OK && Array.isArray(response?.data?.variables)) { + //create a top-level container to hold all the local vars + const container = this.createEvaluateContainer( + //dummy data + { + isConst: false, + isContainer: true, + keyType: VariableType.String, + refCount: undefined, + type: VariableType.AssociativeArray, + value: undefined, + children: response.data.variables + }, + //no name, this is a dummy container + undefined, + //there's no parent path + undefined + ); + return container; + } + } - if (variableType === 'Subtyped_Object') { - //subtyped objects can only have string values - let parts = (variable.value as string).split('; '); - variableType = `${parts[0]} (${parts[1]})`; - } else if (variableType === 'AA') { - variableType = 'AssociativeArray'; - } + /** + * Create an EvaluateContainer for the given variable. If the variable has children, those are created and attached as well + * @param variable a Variable object from the debug protocol debugger + * @param name the name of this variable. For example, `alpha.beta.charlie`, this value would be `charlie`. For local vars, this is the root variable name (i.e. `alpha`) + * @param parentEvaluateName the string used to derive the parent, _excluding_ this variable's name (i.e. `alpha.beta` or `alpha[0]`) + */ + private createEvaluateContainer(variable: Variable, name: string, parentEvaluateName: string) { + let value; + let variableType = variable.type; + if (variable.value === null) { + value = 'roInvalid'; + } else if (variableType === 'String') { + value = `\"${variable.value}\"`; + } else { + value = variable.value; + } - let container = { - name: expression, - evaluateName: expression, - variablePath: variablePath, - type: variableType, - value: value, - keyType: variable.keyType, - elementCount: variable.elementCount - }; + if (variableType === VariableType.SubtypedObject) { + //subtyped objects can only have string values + let parts = (variable.value as string).split('; '); + (variableType as string) = `${parts[0]} (${parts[1]})`; + } else if (variableType === VariableType.AssociativeArray) { + variableType = VariableType.AssociativeArray; + } - if (!firstHandled && variablePath.length > 0) { - firstHandled = true; - mainContainer = container; - } else { - if (!firstHandled && variablePath.length === 0) { - // If this is a scope request there will be no entries in the variable path - // We will need to create a fake mainContainer - firstHandled = true; - mainContainer = { - name: expression, - evaluateName: expression, - variablePath: variablePath, - type: '', - value: null, - keyType: 'String', - elementCount: response.numVariables - }; - } + //build full evaluate name for this var. (i.e. `alpha["beta"]` + ["charlie"]` === `alpha["beta"]["charlie"]`) + let evaluateName: string; + if (!parentEvaluateName?.trim()) { + evaluateName = name; + } else if (typeof name === 'string') { + evaluateName = `${parentEvaluateName}["${name}"]`; + } else if (typeof name === 'number') { + evaluateName = `${parentEvaluateName}[${name}]`; + } - let pathAddition = mainContainer.keyType === 'Integer' ? children.length : variable.name; - container.name = pathAddition.toString(); - if (mainContainer.evaluateName) { - container.evaluateName = `${mainContainer.evaluateName}["${pathAddition}"]`; - } else { - container.evaluateName = pathAddition.toString(); - } - container.variablePath = [].concat(container.variablePath, [pathAddition.toString()]); - if (container.keyType) { - container.children = []; - } - children.push(container); - } + let container: EvaluateContainer = { + name: name ?? '', + evaluateName: evaluateName ?? '', + type: variableType ?? '', + value: value ?? null, + highLevelType: undefined, + //non object/array variables don't have a key type + keyType: variable.keyType as unknown as KeyType, + elementCount: variable.childCount ?? variable.children?.length ?? undefined, + //non object/array variables still need to have an empty `children` array to help upstream logic. The `keyType` being null is how we know it doesn't actually have children + children: [] + }; + + //recursively generate children containers + if ([KeyType.integer, KeyType.string].includes(container.keyType) && Array.isArray(variable.children)) { + container.children = []; + for (let i = 0; i < variable.children.length; i++) { + const childVariable = variable.children[i]; + const childContainer = this.createEvaluateContainer( + childVariable, + container.keyType === KeyType.integer ? i.toString() : childVariable.name, + container.evaluateName + ); + container.children.push(childContainer); } - mainContainer.children = children; - return mainContainer; } + return container; } /** @@ -582,18 +689,18 @@ export class DebugProtocolAdapter { } return this.resolve('threads', async () => { let threads: Thread[] = []; - let threadsData = await this.socketDebugger.threads(); + let threadsResponse = await this.socketDebugger.threads(); - for (let i = 0; i < threadsData.threadsCount; i++) { - let threadInfo = threadsData.threads[i]; + for (let i = 0; i < threadsResponse.data?.threads?.length ?? 0; i++) { + let threadInfo = threadsResponse.data.threads[i]; let thread = { // NOTE: On THREAD_ATTACHED events the threads request is marking the wrong thread as primary. // NOTE: Rely on the thead index from the threads update event. isSelected: this.socketDebugger.primaryThread === i, // isSelected: threadInfo.isPrimary, - filePath: threadInfo.fileName, + filePath: threadInfo.filePath, functionName: threadInfo.functionName, - lineNumber: threadInfo.lineNumber + 1, //protocol is 1-based + lineNumber: threadInfo.lineNumber, //threadInfo.lineNumber is 1-based. Thread requires 1-based line numbers lineContents: threadInfo.codeSnippet, threadId: i }; @@ -618,22 +725,33 @@ export class DebugProtocolAdapter { } public removeAllListeners() { - this.emitter?.removeAllListeners(); + if (this.emitter) { + this.emitter.removeAllListeners(); + } } + /** + * Indicates whether this class had `.destroy()` called at least once. Mostly used for checking externally to see if + * the whole debug session has been terminated or is in a bad state. + */ + public isDestroyed = false; /** * Disconnect from the telnet session and unset all objects */ public async destroy() { + this.isDestroyed = true; + + // destroy the debug client if it's defined if (this.socketDebugger) { - // destroy might be called due to a compile error so the socket debugger might not exist yet - await this.socketDebugger.exitChannel(); + try { + await this.socketDebugger.destroy(); + } catch (e) { + this.logger.error(e); + } } this.cache = undefined; - if (this.emitter) { - this.emitter.removeAllListeners(); - } + this.removeAllListeners(); this.emitter = undefined; } @@ -660,25 +778,41 @@ export class DebugProtocolAdapter { this.chanperfTracker.clearHistory(); } + private syncBreakpointsPromise = Promise.resolve(); public async syncBreakpoints() { + this.logger.log('syncBreakpoints()'); + //wait for the previous sync to finish + this.syncBreakpointsPromise = this.syncBreakpointsPromise + //ignore any errors + .catch(() => { }) + //run the next sync + .then(() => this._syncBreakpoints()); + + //return the new promise, which will resolve once our latest `syncBreakpoints()` call is finished + return this.syncBreakpointsPromise; + } + + public async _syncBreakpoints() { //we can't send breakpoints unless we're stopped (or in a protocol version that supports sending them while running). //So...if we're not stopped, quit now. (we'll get called again when the stop event happens) - if (!this.socketDebugger.supportsBreakpointRegistrationWhileRunning && !this.isAtDebuggerPrompt) { + if (!this.socketDebugger?.supportsBreakpointRegistrationWhileRunning && !this.isAtDebuggerPrompt) { return; } //compute breakpoint changes since last sync const diff = await this.breakpointManager.getDiff(this.projectManager.getAllProjects()); + this.logger.log('Syncing breakpoints', diff); - //delete these breakpoints + // REMOVE breakpoints (delete these breakpoints from the device) if (diff.removed.length > 0) { - await this.actionQueue.run(async () => { - const response = await this.socketDebugger.removeBreakpoints( - diff.removed.map(x => x.deviceId) - ); - //return true to mark this action as complete, or false to retry the task again in the future - return response.success && response.errorCode === ERROR_CODES.OK; - }); + const response = await this.socketDebugger.removeBreakpoints( + //TODO handle retrying to remove breakpoints that don't have deviceIds yet but might get one in the future + diff.removed.map(x => x.deviceId).filter(x => typeof x === 'number') + ); + + if (response.data?.errorCode === ErrorCode.NOT_STOPPED) { + this.breakpointManager.failedDeletions.push(...diff.removed); + } } if (diff.added.length > 0) { @@ -689,50 +823,55 @@ export class DebugProtocolAdapter { lineNumber: breakpoint.line, hitCount: !isNaN(hitCount) ? hitCount : undefined, conditionalExpression: breakpoint.condition, - key: breakpoint.hash, + srcHash: breakpoint.srcHash, + destHash: breakpoint.destHash, componentLibraryName: breakpoint.componentLibraryName }; }); - //send these new breakpoints to the device - await this.actionQueue.run(async () => { - //split the list into conditional and non-conditional breakpoints. - //(TODO we can eliminate this splitting logic once the conditional breakpoints "continue" bug in protocol is fixed) - const standardBreakpoints: typeof breakpointsToSendToDevice = []; - const conditionalBreakpoints: typeof breakpointsToSendToDevice = []; - for (const breakpoint of breakpointsToSendToDevice) { - if (breakpoint?.conditionalExpression?.trim()) { - conditionalBreakpoints.push(breakpoint); - } else { - standardBreakpoints.push(breakpoint); - } + //split the list into conditional and non-conditional breakpoints. + //(TODO we can eliminate this splitting logic once the conditional breakpoints "continue" bug in protocol is fixed) + const standardBreakpoints: typeof breakpointsToSendToDevice = []; + const conditionalBreakpoints: typeof breakpointsToSendToDevice = []; + for (const breakpoint of breakpointsToSendToDevice) { + if (breakpoint?.conditionalExpression?.trim()) { + conditionalBreakpoints.push(breakpoint); + } else { + standardBreakpoints.push(breakpoint); } - let success = true; - for (const breakpoints of [standardBreakpoints, conditionalBreakpoints]) { - const response = await this.socketDebugger.addBreakpoints(breakpoints); - if (response.errorCode === ERROR_CODES.OK) { - //mark the breakpoints as verified - for (let i = 0; i < response.breakpoints.length; i++) { - const deviceBreakpoint = response.breakpoints[i]; + } + for (const breakpoints of [standardBreakpoints, conditionalBreakpoints]) { + const response = await this.socketDebugger.addBreakpoints(breakpoints); + + //if the response was successful, and we have the correct number of breakpoints in the response + if (response.data.errorCode === ErrorCode.OK && response?.data?.breakpoints?.length === breakpoints.length) { + for (let i = 0; i < response?.data?.breakpoints?.length ?? 0; i++) { + const deviceBreakpoint = response.data.breakpoints[i]; + + if (typeof deviceBreakpoint?.id === 'number') { //sync this breakpoint's deviceId with the roku-assigned breakpoint ID this.breakpointManager.setBreakpointDeviceId( - breakpoints[i].key, - deviceBreakpoint.breakpointId + breakpoints[i].srcHash, + breakpoints[i].destHash, + deviceBreakpoint.id ); } - //return true to mark this action as complete - success &&= true; - } else { - //this action is not yet complete. it should be retried - success &&= false; + + //this breakpoint had an issue. remove it from the client + if (deviceBreakpoint.errorCode !== ErrorCode.OK) { + this.breakpointManager.deleteBreakpoint(breakpoints[i].srcHash); + } } + //the entire response was bad. delete these breakpoints from the client + } else { + this.breakpointManager.deleteBreakpoints( + breakpoints.map(x => x.srcHash) + ); } - return success; - }); + + } } } - - private actionQueue = new ActionQueue(); } export interface StackFrame { @@ -751,10 +890,9 @@ export enum EventName { export interface EvaluateContainer { name: string; evaluateName: string; - variablePath: string[]; type: string; value: string; - keyType: KeyType; + keyType?: KeyType; elementCount: number; highLevelType: HighLevelType; children: EvaluateContainer[]; @@ -769,6 +907,9 @@ export enum KeyType { export interface Thread { isSelected: boolean; + /** + * The 1-based line number for the thread + */ lineNumber: number; filePath: string; functionName: string; @@ -780,3 +921,7 @@ interface BrightScriptRuntimeError { message: string; errorCode: string; } + +export function isDebugProtocolAdapter(adapter: TelnetAdapter | DebugProtocolAdapter): adapter is DebugProtocolAdapter { + return adapter?.constructor.name === DebugProtocolAdapter.name; +} diff --git a/src/adapters/TelnetAdapter.ts b/src/adapters/TelnetAdapter.ts index 9a85c829..47b04eed 100644 --- a/src/adapters/TelnetAdapter.ts +++ b/src/adapters/TelnetAdapter.ts @@ -14,6 +14,7 @@ import { logger } from '../logging'; import type { AdapterOptions, RokuAdapterEvaluateResponse } from '../interfaces'; import { HighLevelType } from '../interfaces'; import { TelnetRequestPipeline } from './TelnetRequestPipeline'; +import type { DebugProtocolAdapter } from './DebugProtocolAdapter'; /** * A class that connects to a Roku device over telnet debugger port and provides a standardized way of interacting with it. @@ -41,7 +42,7 @@ export class TelnetAdapter { }); } - public logger = logger.createLogger(`[${TelnetAdapter.name}]`); + public logger = logger.createLogger(`[tadapter]`); /** * Indicates whether the adapter has successfully established a connection with the device */ @@ -510,10 +511,9 @@ export class TelnetAdapter { /** * Gets a string array of all the local variables using the var command - * @param scope */ - public async getScopeVariables(scope?: string) { - this.logger.log('getScopeVariables', { scope }); + public async getScopeVariables() { + this.logger.log('getScopeVariables'); if (!this.isAtDebuggerPrompt) { throw new Error('Cannot resolve variable: debugger is not paused'); } @@ -867,7 +867,6 @@ export class TelnetAdapter { type: '', highLevelType: HighLevelType.uninitialized, evaluateName: undefined, - variablePath: [], elementCount: -1, value: '', keyType: KeyType.legacy, @@ -1008,10 +1007,17 @@ export class TelnetAdapter { this.emitter?.removeAllListeners(); } + /** + * Indicates whether this class has had `.destroy()` called at least once. Mostly used for checking externally to see if + * the whole debug session has been terminated or is in a bad state. + */ + public isDestroyed = false; /** * Disconnect from the telnet session and unset all objects */ public destroy() { + this.isDestroyed = true; + if (this.requestPipeline) { this.requestPipeline.destroy(); } @@ -1068,7 +1074,6 @@ export enum EventName { export interface EvaluateContainer { name: string; evaluateName: string; - variablePath: string[]; type: string; value: string; keyType: KeyType; @@ -1119,3 +1124,7 @@ interface BrightScriptRuntimeError { message: string; errorCode: string; } + +export function isTelnetAdapterAdapter(adapter: TelnetAdapter | DebugProtocolAdapter): adapter is TelnetAdapter { + return adapter?.constructor.name === TelnetAdapter.name; +} diff --git a/src/debugProtocol/Constants.ts b/src/debugProtocol/Constants.ts index 34d20b2d..dacc6ca8 100644 --- a/src/debugProtocol/Constants.ts +++ b/src/debugProtocol/Constants.ts @@ -4,30 +4,110 @@ export enum PROTOCOL_ERROR_CODES { NOT_SUPPORTED = 2 } -export enum COMMANDS { - STOP = 1, - CONTINUE = 2, - THREADS = 3, - STACKTRACE = 4, - VARIABLES = 5, - STEP = 6, // Since protocol 1.1 - ADD_BREAKPOINTS = 7, // since protocol 1.2 - LIST_BREAKPOINTS = 8, // since protocol 1.2 - REMOVE_BREAKPOINTS = 9, // since protocol 1.2 - EXECUTE = 10, // since protocol 2.1 - ADD_CONDITIONAL_BREAKPOINTS = 11, // since protocol 3.1 - EXIT_CHANNEL = 122 +/** + * The name of commands that can be sent from the client to the server. Think of these like "requests". + */ +export enum Command { + /** + * Stop all threads in application. Enter into debugger. + * + * Individual threads can not be stopped/started. + */ + Stop = 'Stop', + /** + * Exit from debugger and continue execution of all threads. + */ + Continue = 'Continue', + /** + * Application threads info + */ + Threads = 'Threads', + /** + * Get the stack trace of a specific thread. + */ + StackTrace = 'StackTrace', + /** + * Listing of variables accessible from selected thread and stack frame. + */ + Variables = 'Variables', + /** + * Execute one step on a specified thread. + * + */ + Step = 'Step', + /** + * Add a dynamic breakpoint. + * + * @since protocol 2.0.0 (Roku OS 9.3) + */ + AddBreakpoints = 'AddBreakpoints', + /** + * Lists existing dynamic and conditional breakpoints and their status. + * + * @since protocol 2.0.0 (Roku OS 9.3) + */ + ListBreakpoints = 'ListBreakpoints', + /** + * Removes dynamic breakpoints. + * + * @since protocol 2.0.0 (Roku OS 9.3) + */ + RemoveBreakpoints = 'RemoveBreakpoints', + /** + * Executes code in a specific stack frame. + * + * @since protocol 2.1 (Roku OS 10.5) + */ + Execute = 'Execute', + /** + * Adds a conditional breakpoint. + * + * @since protocol 3.1.0 (Roku OS 11.5) + */ + AddConditionalBreakpoints = 'AddConditionalBreakpoints', + /** + * + */ + ExitChannel = 'ExitChannel' +} +/** + * Only used for serializing/deserializing over the debug protocol. Use `Command` in your code. + */ +export enum CommandCode { + Stop = 1, + Continue = 2, + Threads = 3, + StackTrace = 4, + Variables = 5, + Step = 6, + AddBreakpoints = 7, + ListBreakpoints = 8, + RemoveBreakpoints = 9, + Execute = 10, + AddConditionalBreakpoints = 11, + ExitChannel = 122 } -export enum STEP_TYPE { - STEP_TYPE_NONE = 0, - STEP_TYPE_LINE = 1, - STEP_TYPE_OUT = 2, - STEP_TYPE_OVER = 3 +/** + * Contains an a StepType enum, indicating the type of step action to be executed. + */ +export enum StepType { + None = 'None', + Line = 'Line', + Out = 'Out', + Over = 'Over' +} +/** + * Only used for serializing/deserializing over the debug protocol. Use `StepType` in your code. + */ +export enum StepTypeCode { + None = 0, + Line = 1, + Out = 2, + Over = 3 } -//#region RESPONSE CONSTS -export enum ERROR_CODES { +export enum ErrorCode { OK = 0, OTHER_ERR = 1, UNDEFINED_COMMAND = 2, @@ -36,107 +116,104 @@ export enum ERROR_CODES { INVALID_ARGS = 5 } -export enum STOP_REASONS { - UNDEFINED = 0, - NOT_STOPPED = 1, - NORMAL_EXIT = 2, - STOP_STATEMENT = 3, - BREAK = 4, - RUNTIME_ERROR = 5 +export enum ErrorFlags { + INVALID_VALUE_IN_PATH = 0x0001, + MISSING_KEY_IN_PATH = 0x0002 } -export enum UPDATE_TYPES { - UNDEF = 0, - IO_PORT_OPENED = 1, // client needs to connect to port to retrieve channel output - ALL_THREADS_STOPPED = 2, - THREAD_ATTACHED = 3, +export enum StopReason { /** - * A compilation or runtime error occurred when evaluating the cond_expr of a conditional breakpoint - * @since protocol 3.1 + * Uninitialized stopReason. */ - BREAKPOINT_ERROR = 4, + Undefined = 'Undefined', /** - * A compilation error occurred - * @since protocol 3.1 + * Thread is running. */ - COMPILE_ERROR = 5, + NotStopped = 'NotStopped', /** - * Breakpoints were successfully verified - * @since protocol 3.2 + * Thread exited. + */ + NormalExit = 'NormalExit', + /** + * Stop statement executed. + */ + StopStatement = 'StopStatement', + /** + * Another thread in the group encountered an error, this thread completed a step operation, or other reason outside this thread. + */ + Break = 'Break', + /** + * Thread stopped because of an error during execution. */ - BREAKPOINT_VERIFIED = 6 + RuntimeError = 'RuntimeError' } - -export enum VARIABLE_REQUEST_FLAGS { - GET_CHILD_KEYS = 0x01, - CASE_SENSITIVITY_OPTIONS = 0x02 +/** + * Only used for serializing/deserializing over the debug protocol. Use `StopReason` in your code. + */ +export enum StopReasonCode { + Undefined = 0, + NotStopped = 1, + NormalExit = 2, + StopStatement = 3, + Break = 4, + RuntimeError = 5 } -export enum VARIABLE_FLAGS { - /** - * value is a child of the requested variable - * e.g., an element of an array or field of an AA - */ - isChildKey = 0x01, +/** + * Human-readable UpdateType values. To get the codes, use the `UpdateTypeCode` enum + */ +export enum UpdateType { + Undefined = 'Undefined', /** - * value is constant + * The remote debugging client should connect to the port included in the data field to retrieve the running script's output. Only reads are allowed on the I/O connection. */ - isConst = 0x02, + IOPortOpened = 'IOPortOpened', /** - * The referenced value is a container (e.g., a list or array) + * All threads are stopped and an ALL_THREADS_STOPPED message is sent to the debugging client. + * + * The data field includes information on why the threads were stopped. */ - isContainer = 0x04, + AllThreadsStopped = 'AllThreadsStopped', /** - * The name is included in this VariableInfo + * A new thread attempts to execute a script when all threads have already been stopped. The new thread is immediately stopped and is "attached" to the + * debugger so that the debugger can inspect the thread, its stack frames, and local variables. + * + * Additionally, when a thread executes a step operation, that thread detaches from the debugger temporarily, + * and a THREAD_ATTACHED message is sent to the debugging client when the thread has completed its step operation and has re-attached to the debugger. + * + * The data field includes information on why the threads were stopped */ - isNameHere = 0x08, + ThreadAttached = 'ThreadAttached', /** - * value is reference-counted. + * A compilation or runtime error occurred when evaluating the cond_expr of a conditional breakpoint + * @since protocol 3.1 */ - isRefCounted = 0x10, + BreakpointError = 'BreakpointError', /** - * value is included in this VariableInfo + * A compilation error occurred + * @since protocol 3.1 */ - isValueHere = 0x20, + CompileError = 'CompileError', /** - * Value is container, key lookup is case sensitive - * @since protocol 3.1.0 + * Breakpoints were successfully verified + * @since protocol 3.2 */ - isKeysCaseSensitive = 0x40 + BreakpointVerified = 'BreakpointVerified' } - -export enum VARIABLE_TYPES { - AA = 1, - Array = 2, - Boolean = 3, - Double = 4, - Float = 5, - Function = 6, - Integer = 7, - Interface = 8, - Invalid = 9, - List = 10, - Long_Integer = 11, - Object = 12, - String = 13, - Subroutine = 14, - Subtyped_Object = 15, - Uninitialized = 16, - Unknown = 17 +/** + * The integer values for `UPDATE_TYPE`. Only used for serializing/deserializing over the debug protocol. Use `UpdateType` in your code. + */ +export enum UpdateTypeCode { + Undefined = 0, + IOPortOpened = 1, + AllThreadsStopped = 2, + ThreadAttached = 3, + BreakpointError = 4, + CompileError = 5, + // /** + // * Breakpoints were successfully verified + // * @since protocol 3.2 + // */ + BreakpointVerified = 6 } -//#endregion -export function getUpdateType(value: number): UPDATE_TYPES { - switch (value) { - case UPDATE_TYPES.ALL_THREADS_STOPPED: - return UPDATE_TYPES.ALL_THREADS_STOPPED; - case UPDATE_TYPES.IO_PORT_OPENED: - return UPDATE_TYPES.IO_PORT_OPENED; - case UPDATE_TYPES.THREAD_ATTACHED: - return UPDATE_TYPES.THREAD_ATTACHED; - case UPDATE_TYPES.UNDEF: - return UPDATE_TYPES.UNDEF; - default: - return UPDATE_TYPES.UNDEF; - } -} diff --git a/src/debugProtocol/DebugProtocolClientReplaySession.spec.ts b/src/debugProtocol/DebugProtocolClientReplaySession.spec.ts new file mode 100644 index 00000000..ee6f83bf --- /dev/null +++ b/src/debugProtocol/DebugProtocolClientReplaySession.spec.ts @@ -0,0 +1,44 @@ +import { expect } from 'chai'; +import { DebugProtocolClientReplaySession } from './DebugProtocolClientReplaySession'; +import type { ProtocolRequest, ProtocolResponse, ProtocolUpdate } from './events/ProtocolEvent'; +import * as fsExtra from 'fs-extra'; + +describe(DebugProtocolClientReplaySession.name, () => { + let session: DebugProtocolClientReplaySession; + + afterEach(async () => { + await session.destroy(); + }); + + it.skip('debug this debugger.log file', async function test() { + this.timeout(10000000000); + const logPath = 'C:/users/bronley/downloads/2023-06-01T12∶21∶04-debugger.log'; + session = new DebugProtocolClientReplaySession({ + bufferLog: fsExtra.readFileSync(logPath).toString() + }); + + await session.run(); + expectClientReplayResult([], session.result); + console.log(session); + }); +}); + +// eslint-disable-next-line @typescript-eslint/ban-types +function expectClientReplayResult(expected: Array, result: DebugProtocolClientReplaySession['result']) { + expected = expected.map(x => { + if (typeof x === 'function') { + return x?.name; + } + return x; + }); + let sanitizedResult = result.map((x, i) => { + //if there is no expected object for this entry, or it's a constructor, then we will compare the constructor name + if (expected[i] === undefined || typeof expected[i] === 'string') { + return x?.constructor?.name; + //deep compare the actual object + } else { + return x; + } + }); + expect(sanitizedResult).to.eql(expected); +} diff --git a/src/debugProtocol/DebugProtocolClientReplaySession.ts b/src/debugProtocol/DebugProtocolClientReplaySession.ts new file mode 100644 index 00000000..e878eec6 --- /dev/null +++ b/src/debugProtocol/DebugProtocolClientReplaySession.ts @@ -0,0 +1,328 @@ +import { DebugProtocolClient } from './client/DebugProtocolClient'; +import { defer, util } from '../util'; +import type { ProtocolRequest, ProtocolResponse, ProtocolUpdate } from './events/ProtocolEvent'; +import { DebugProtocolServer } from './server/DebugProtocolServer'; +import * as Net from 'net'; +import { ActionQueue } from '../managers/ActionQueue'; +import { IOPortOpenedUpdate, isIOPortOpenedUpdate } from './events/updates/IOPortOpenedUpdate'; + +export class DebugProtocolClientReplaySession { + constructor(options: { + bufferLog: string; + }) { + this.parseBufferLog(options?.bufferLog); + } + + private disposables = Array<() => void>(); + + /** + * A dumb tcp server that will simply spit back the server buffer data when needed + */ + private server: Net.Socket; + + private ioSocket: Net.Socket; + + private client: DebugProtocolClient; + + private entryIndex = 0; + private entries: Array; + + private peekEntry() { + this.flushIO(); + return this.entries[this.entryIndex]; + } + private advanceEntry() { + this.flushIO(); + return this.entries[this.entryIndex++]; + } + + private flushIO() { + while (this.entries[this.entryIndex]?.type === 'io') { + const entry = this.entries[this.entryIndex++]; + this.ioSocket.write(entry.buffer); + } + } + + private parseBufferLog(bufferLog: string) { + this.entries = bufferLog + .split(/\r?\n/g) + //only keep lines that include the `[[bufferLog]]` magic text + .filter(x => x.includes('[[bufferLog]]')) + //remove leading text, leaving only the raw bufferLog entry + .map(x => x.replace(/.*?\[\[bufferLog\]\]:/, '').trim()) + .filter(x => !!x) + .map(line => { + const entry = JSON.parse(line); + entry.timestamp = new Date(entry.timestamp as string); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + entry.buffer = Buffer.from(entry.buffer); + return entry; + }); + return this.entries; + } + + public result: Array = []; + private finished = defer(); + private controlPort: number; + private ioPort: number; + + public async run() { + this.controlPort = await util.getPort(); + this.ioPort = await util.getPort(); + + await this.createServer(this.controlPort); + + this.createClient(this.controlPort); + + //connect, but don't send the handshake. That'll be send through our first server-to-client entry (hopefully) + await this.client.connect(false); + + void this.clientProcess(); + await this.finished.promise; + } + + private createClient(controlPort: number) { + this.client = new DebugProtocolClient({ + controlPort: controlPort, + host: 'localhost' + }); + + //store the responses in the result + this.client.on('response', (response) => { + this.result.push(response); + void this.clientProcess(); + }); + this.client.on('update', (update) => { + this.result.push(update); + void this.clientProcess(); + }); + + this.client.on('io-output', (data) => { + console.log(data); + void this.clientProcess(); + }); + + //anytime the client receives buffer data, we should try and process it + this.client.on('data', (data) => { + this.clientSync.pushActual(data); + void this.clientProcess(); + }); + + this.client.plugins.add({ + beforeHandleUpdate: async (event) => { + if (isIOPortOpenedUpdate(event.update)) { + //spin up an IO port before finishing this update + await this.openIOPort(); + + const update = IOPortOpenedUpdate.fromJson(event.update.data); + update.data.port = this.ioPort; + //if we get an IO update, change the port and host to the local stuff (for testing purposes) + event.update = update; + } + } + }); + + //stuff to run when the session is disposed + this.disposables.push(() => { + return this.client.destroy(); + }); + } + + private openIOPort() { + console.log(`Spinning up mock IO socket on port ${this.ioPort}`); + return new Promise((resolve) => { + const server = new Net.Server({}); + + //whenever a client makes a connection + // eslint-disable-next-line @typescript-eslint/no-misused-promises + server.on('connection', (client: Net.Socket) => { + this.ioSocket = client; + //anytime we receive incoming data from the client + client.on('data', (data) => { + //TODO send IO data + }); + }); + server.listen({ + port: this.ioPort, + hostName: 'localhost' + }, () => { + resolve(); + }); + + //stuff to run when the session is disposed + this.disposables.push(() => { + server.close(); + }); + this.disposables.push(() => { + this.ioSocket?.destroy(); + }); + }); + } + + private clientSync = new BufferSync(); + + private clientActionQueue = new ActionQueue(); + + private async clientProcess() { + await this.clientActionQueue.run(async () => { + //build a single buffer of client data + while (this.peekEntry()?.type === 'client-to-server') { + //make sure it's been enough time since the last entry + await this.sleepForEntryGap(); + const entry = this.advanceEntry(); + const request = DebugProtocolServer.getRequest(entry.buffer, true); + + //store this client data for our mock server to recognize and track + this.serverSync.pushExpected(request.toBuffer()); + + //store the request in the result + this.result.push(request); + + //send the request + void this.client.processRequest(request); + + } + this.finalizeIfDone(); + return true; + }); + } + + private finalizeIfDone() { + if (this.clientSync.areInSync && this.serverSync.areInSync && this.entryIndex >= this.entries.length) { + this.finished.resolve(); + } + } + + private createServer(controlPort: number) { + return new Promise((resolve) => { + + const server = new Net.Server({}); + //Roku only allows 1 connection, so we should too. + server.maxConnections = 1; + + //whenever a client makes a connection + // eslint-disable-next-line @typescript-eslint/no-misused-promises + server.on('connection', (client: Net.Socket) => { + this.server = client; + //anytime we receive incoming data from the client + client.on('data', (data) => { + console.log('server got:', JSON.stringify(data.toJSON().data)); + void this.serverProcess(data); + }); + }); + server.listen({ + port: controlPort, + hostName: 'localhost' + }, () => { + resolve(); + }); + + //stuff to run when the session is disposed + this.disposables.push(() => { + server.close(); + }); + this.disposables.push(() => { + this.client?.destroy(); + }); + }); + } + + private serverActionQueue = new ActionQueue(); + + private serverSync = new BufferSync(); + private serverProcessIdx = 0; + private async serverProcess(data: Buffer) { + let serverProcesIdx = this.serverProcessIdx++; + await this.serverActionQueue.run(async () => { + try { + console.log(serverProcesIdx); + this.serverSync.pushActual(data); + if (this.serverSync.areInSync) { + this.serverSync.clear(); + //send all the server messages, each delayed slightly to simulate the chunked buffer flushing that roku causes + while (this.peekEntry()?.type === 'server-to-client') { + //make sure enough time has passed since the last entry + await this.sleepForEntryGap(); + const entry = this.advanceEntry(); + this.server.write(entry.buffer); + this.clientSync.pushExpected(entry.buffer); + } + } + this.finalizeIfDone(); + } catch (e) { + console.error('serverProcess failed to handle buffer', e); + } + return true; + }); + } + + /** + * Sleep for the amount of time between the two specified entries + */ + private async sleepForEntryGap() { + const currentEntry = this.entries[this.entryIndex]; + const previousEntry = this.entries[this.entryIndex - 1]; + let gap = 0; + if (currentEntry && previousEntry) { + gap = currentEntry.timestamp.getTime() - previousEntry?.timestamp.getTime(); + //if the gap is negative, then the time has already passed. Just timeout at zero + gap = gap > 0 ? gap : 0; + } + //longer delays make the test run slower, but don't really make the test any more accurate, + //so cap the delay at 100ms + if (gap > 100) { + gap = 100; + } + await util.sleep(gap); + } + + public async destroy() { + for (const dispose of this.disposables) { + try { + await Promise.resolve(dispose()); + } catch { } + } + } +} + +class BufferSync { + private expected = Buffer.alloc(0); + public pushExpected(buffer: Buffer) { + this.expected = Buffer.concat([this.expected, buffer]); + } + + private actual = Buffer.alloc(0); + public pushActual(buffer: Buffer) { + this.actual = Buffer.concat([this.actual, buffer]); + } + + /** + * Are the two buffers in sync? + */ + public get areInSync() { + return JSON.stringify(this.expected) === JSON.stringify(this.actual); + } + + public clear() { + this.expected = Buffer.alloc(0); + this.actual = Buffer.alloc(0); + } +} + +function bufferStartsWith(subject: Buffer, search: Buffer) { + const subjectData = subject.toJSON().data; + const searchData = search.toJSON().data; + for (let i = 0; i < searchData.length; i++) { + if (subjectData[i] !== searchData[i]) { + return false; + } + } + //if we made it to the end of the search, then the subject fully starts with search + return true; +} + +export interface BufferLogEntry { + type: 'client-to-server' | 'server-to-client' | 'io'; + timestamp: Date; + buffer: Buffer; +} diff --git a/src/debugProtocol/DebugProtocolServerTestPlugin.spec.ts b/src/debugProtocol/DebugProtocolServerTestPlugin.spec.ts new file mode 100644 index 00000000..c52ec710 --- /dev/null +++ b/src/debugProtocol/DebugProtocolServerTestPlugin.spec.ts @@ -0,0 +1,119 @@ +import { ConsoleTransport, Logger } from '@rokucommunity/logger'; +import { createLogger } from '../logging'; +import { isProtocolUpdate } from './client/DebugProtocolClient'; +import type { ProtocolResponse, ProtocolRequest, ProtocolUpdate } from './events/ProtocolEvent'; +import { HandshakeRequest } from './events/requests/HandshakeRequest'; +import type { DebugProtocolServer } from './server/DebugProtocolServer'; +import type { BeforeSendResponseEvent, OnServerStartEvent, ProtocolServerPlugin, ProvideResponseEvent } from './server/DebugProtocolServerPlugin'; + +/** + * A class that intercepts all debug server events and provides test data for them + */ +export class DebugProtocolServerTestPlugin implements ProtocolServerPlugin { + + /** + * A list of responses or updates to be sent by the server in this exact order. + * One of these will be sent for every `provideResponse` event received. Any leading ProtocolUpdate entries will be sent as soon as seen. + * For example, if the array is `[Update1, Update2, Response1, Update3]`, when the `provideResponse` event is triggered, we will first send + * `Update1` and `Update2`, then provide `Response1`. `Update3` will be triggered when the next `provideResponse` is requested, or if `.flush()` is called + */ + private responseUpdateQueue: Array = []; + + /** + * Adds a response to the queue, which should be returned from the server in first-in-first-out order, one for each request received by the server + */ + public pushResponse(event: ProtocolResponse) { + this.responseUpdateQueue.push(event); + } + + /** + * Adds a ProtocolUpdate to the queue. Any leading updates are send to the client anytime `provideResponse` is triggered, or when `.flush()` is called + */ + public pushUpdate(event: ProtocolUpdate) { + this.responseUpdateQueue.push(event); + } + + /** + * A running list of requests received by the server during this test + */ + public readonly requests: ReadonlyArray = []; + + /** + * The most recent request received by the plugin + */ + public get latestRequest() { + return this.requests[this.requests.length - 1]; + } + + public getLatestRequest() { + return this.latestRequest as unknown as T; + } + + /** + * Get the request at the specified index. Negative indexes count back from the last item in the array + */ + public getRequest(index: number) { + if (index < 0) { + //add the negative index to the length to "subtract" from the end + index = this.requests.length + index; + } + return this.requests[index] as T; + } + + /** + * A running list of responses sent by the server during this test + */ + public readonly responses: ReadonlyArray = []; + + /** + * The most recent response received by the plugin + */ + public get latestResponse() { + return this.responses[this.responses.length - 1]; + } + + public server: DebugProtocolServer; + + /** + * Fired whenever the server starts up + */ + onServerStart({ server }: OnServerStartEvent) { + this.server = server; + } + + /** + * Flush all leading updates in the queue + */ + public async flush() { + while (isProtocolUpdate(this.responseUpdateQueue[0])) { + await this.server.sendUpdate(this.responseUpdateQueue.shift()); + } + } + + /** + * Whenever the server receives a request, this event allows us to send back a response + */ + async provideResponse(event: ProvideResponseEvent) { + //store the request for testing purposes + (this.requests as Array).push(event.request); + + //flush leading updates + await this.flush(); + + const response = this.responseUpdateQueue.shift(); + //if there's no response, AND this isn't the handshake, fail. (we want the protocol to handle the handshake most of the time) + if (!response && !(event.request instanceof HandshakeRequest)) { + throw new Error(`There was no response available to send back for ${event.request.constructor.name}`); + } + //force this response to have the current request's ID (for testing purposes) + if (response) { + response.data.requestId = event.request.data.requestId; + } + event.response = response; + } + + beforeSendResponse(event: BeforeSendResponseEvent) { + //store the response for testing purposes + (this.responses as Array).push(event.response); + } +} diff --git a/src/debugProtocol/Debugger.spec.ts b/src/debugProtocol/Debugger.spec.ts deleted file mode 100644 index 7869de75..00000000 --- a/src/debugProtocol/Debugger.spec.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { Debugger } from './Debugger'; -import { expect } from 'chai'; -import { SmartBuffer } from 'smart-buffer'; -import { MockDebugProtocolServer } from './MockDebugProtocolServer.spec'; -import { createSandbox } from 'sinon'; -import { createHandShakeResponse, createHandShakeResponseV3, createProtocolEventV3 } from './responses/responseCreationHelpers.spec'; -import { HandshakeResponseV3, ProtocolEventV3 } from './responses'; -import { ERROR_CODES, UPDATE_TYPES, VARIABLE_REQUEST_FLAGS } from './Constants'; -const sinon = createSandbox(); - -describe('debugProtocol Debugger', () => { - let bsDebugger: Debugger; - let roku: MockDebugProtocolServer; - - beforeEach(async () => { - sinon.stub(console, 'log').callsFake((...args) => { }); - roku = new MockDebugProtocolServer(); - await roku.initialize(); - - bsDebugger = new Debugger({ - host: 'localhost', - controllerPort: roku.controllerPort - }); - }); - - afterEach(() => { - bsDebugger?.destroy(); - bsDebugger = undefined; - sinon.restore(); - roku.destroy(); - }); - - describe('connect', () => { - it('sends magic to server on connect', async () => { - let action = roku.waitForMagic(); - void bsDebugger.connect(); - void roku.processActions(); - let magic = await action.promise; - expect(magic).to.equal(Debugger.DEBUGGER_MAGIC); - }); - - it('validates magic from server on connect', async () => { - const magicAction = roku.waitForMagic(); - roku.sendHandshakeResponse(magicAction.promise); - - void bsDebugger.connect(); - - void roku.processActions(); - - //wait for the debugger to finish verifying the handshake - expect( - await bsDebugger.once('handshake-verified') - ).to.be.true; - }); - - it('throws on magic mismatch', async () => { - roku.waitForMagic(); - roku.sendHandshakeResponse('not correct magic'); - - void bsDebugger.connect(); - - void roku.processActions(); - - //wait for the debugger to finish verifying the handshake - expect( - await bsDebugger.once('handshake-verified') - ).to.be.false; - }); - }); - - describe('parseUnhandledData', () => { - it('handles legacy handshake', () => { - let mockResponse = createHandShakeResponse({ - magic: Debugger.DEBUGGER_MAGIC, - major: 1, - minor: 0, - patch: 0 - }); - - bsDebugger['unhandledData'] = mockResponse.toBuffer(); - - expect(bsDebugger.watchPacketLength).to.be.equal(false); - expect(bsDebugger.handshakeComplete).to.be.equal(false); - - expect(bsDebugger['parseUnhandledData'](bsDebugger['unhandledData'])).to.be.equal(true); - - expect(bsDebugger.watchPacketLength).to.be.equal(false); - expect(bsDebugger.handshakeComplete).to.be.equal(true); - expect(bsDebugger['unhandledData'].byteLength).to.be.equal(0); - }); - - it('handles v3 handshake', () => { - let mockResponse = createHandShakeResponseV3({ - magic: Debugger.DEBUGGER_MAGIC, - major: 3, - minor: 0, - patch: 0, - revisionTimeStamp: Date.now() - }); - - bsDebugger['unhandledData'] = mockResponse.toBuffer(); - - expect(bsDebugger.watchPacketLength).to.be.equal(false); - expect(bsDebugger.handshakeComplete).to.be.equal(false); - - expect(bsDebugger['parseUnhandledData'](bsDebugger['unhandledData'])).to.be.equal(true); - - expect(bsDebugger.watchPacketLength).to.be.equal(true); - expect(bsDebugger.handshakeComplete).to.be.equal(true); - expect(bsDebugger['unhandledData'].byteLength).to.be.equal(0); - }); - - it('handles events after handshake', () => { - let handshake = createHandShakeResponseV3({ - magic: Debugger.DEBUGGER_MAGIC, - major: 3, - minor: 0, - patch: 0, - revisionTimeStamp: Date.now() - }); - - let protocolEvent = createProtocolEventV3({ - requestId: 0, - errorCode: ERROR_CODES.CANT_CONTINUE, - updateType: UPDATE_TYPES.ALL_THREADS_STOPPED - }); - - let mockResponse = new SmartBuffer(); - mockResponse.writeBuffer(handshake.toBuffer()); - mockResponse.writeBuffer(protocolEvent.toBuffer()); - - bsDebugger['unhandledData'] = mockResponse.toBuffer(); - - const stub = sinon.stub(bsDebugger as any, 'removedProcessedBytes').callThrough(); - - expect(bsDebugger.watchPacketLength).to.be.equal(false); - expect(bsDebugger.handshakeComplete).to.be.equal(false); - - expect(bsDebugger['parseUnhandledData'](bsDebugger['unhandledData'])).to.be.equal(true); - - expect(bsDebugger.watchPacketLength).to.be.equal(true); - expect(bsDebugger.handshakeComplete).to.be.equal(true); - expect(bsDebugger['unhandledData'].byteLength).to.be.equal(0); - - let calls = stub.getCalls(); - expect(calls[0].args[0]).instanceOf(HandshakeResponseV3); - expect(calls[1].args[0]).instanceOf(ProtocolEventV3); - }); - }); - - describe('getVariables', () => { - function getVariablesRequestBufferToJson(buffer: SmartBuffer) { - const result = { - flags: buffer.readUInt8(), - threadIndex: buffer.readUInt32LE(), - stackFrameIndex: buffer.readUInt32LE(), - variablePathEntries: [], - pathForceCaseInsensitive: [] - }; - - const pathLength = buffer.readUInt32LE(); - if (pathLength > 0) { - result.variablePathEntries = []; - for (let i = 0; i < pathLength; i++) { - result.variablePathEntries.push( - buffer.readBufferNT().toString() - ); - } - } - // eslint-disable-next-line no-bitwise - if (result.flags & VARIABLE_REQUEST_FLAGS.CASE_SENSITIVITY_OPTIONS) { - result.pathForceCaseInsensitive = []; - for (let i = 0; i < pathLength; i++) { - result.pathForceCaseInsensitive.push( - buffer.readUInt8() === 0 ? false : true - ); - } - } - return result; - } - - it('skips case sensitivity info on lower protocol versions', async () => { - bsDebugger.protocolVersion = '2.0.0'; - bsDebugger['stopped'] = true; - const stub = sinon.stub(bsDebugger as any, 'makeRequest').callsFake(() => { }); - await bsDebugger.getVariables(['m', 'top'], false, 1, 2); - expect( - getVariablesRequestBufferToJson(stub.getCalls()[0].args[0]) - ).to.eql({ - flags: 0, - stackFrameIndex: 1, - threadIndex: 2, - variablePathEntries: ['m', 'top'], - //should be empty - pathForceCaseInsensitive: [] - }); - }); - - it('marks strings as case-sensitive', async () => { - bsDebugger.protocolVersion = '3.1.0'; - bsDebugger['stopped'] = true; - const stub = sinon.stub(bsDebugger as any, 'makeRequest').callsFake(() => { }); - await bsDebugger.getVariables(['m', 'top', '"someKey"', '""someKeyWithInternalQuotes""'], true, 1, 2); - expect( - getVariablesRequestBufferToJson(stub.getCalls()[0].args[0]) - ).to.eql({ - // eslint-disable-next-line no-bitwise - flags: VARIABLE_REQUEST_FLAGS.GET_CHILD_KEYS | VARIABLE_REQUEST_FLAGS.CASE_SENSITIVITY_OPTIONS, - stackFrameIndex: 1, - threadIndex: 2, - variablePathEntries: ['m', 'top', 'someKey', '"someKeyWithInternalQuotes"'], - //should be empty - pathForceCaseInsensitive: [true, true, false, false] - }); - }); - }); -}); diff --git a/src/debugProtocol/Debugger.ts b/src/debugProtocol/Debugger.ts deleted file mode 100644 index 4387777c..00000000 --- a/src/debugProtocol/Debugger.ts +++ /dev/null @@ -1,802 +0,0 @@ -import * as Net from 'net'; -import * as EventEmitter from 'eventemitter3'; -import * as semver from 'semver'; -import type { - ThreadAttached, - ThreadsStopped -} from './responses'; -import { - ConnectIOPortResponse, - HandshakeResponse, - HandshakeResponseV3, - ProtocolEvent, - ProtocolEventV3, - StackTraceResponse, - StackTraceResponseV3, - ThreadsResponse, - UndefinedResponse, - UpdateThreadsResponse, - VariableResponse -} from './responses'; -import { PROTOCOL_ERROR_CODES, COMMANDS, STEP_TYPE, STOP_REASONS, VARIABLE_REQUEST_FLAGS, ERROR_CODES, UPDATE_TYPES } from './Constants'; -import { SmartBuffer } from 'smart-buffer'; -import { logger } from '../logging'; -import { ExecuteResponseV3 } from './responses/ExecuteResponseV3'; -import { ListBreakpointsResponse } from './responses/ListBreakpointsResponse'; -import { AddBreakpointsResponse } from './responses/AddBreakpointsResponse'; -import { RemoveBreakpointsResponse } from './responses/RemoveBreakpointsResponse'; -import { util } from '../util'; -import { BreakpointErrorUpdateResponse } from './responses/BreakpointErrorUpdateResponse'; -import type { VerifiedBreakpointsData } from './responses/BreakpointVerifiedUpdateResponse'; -import { BreakpointVerifiedUpdateResponse } from './responses/BreakpointVerifiedUpdateResponse'; - -export class Debugger { - - private logger = logger.createLogger(`[${Debugger.name}]`); - - public get isStopped(): boolean { - return this.stopped; - } - - // The highest tested version of the protocol we support. - public supportedVersionRange = '<=3.0.0'; - - constructor( - options: ConstructorOptions - ) { - this.options = { - controllerPort: 8081, - host: undefined, - //override the defaults with the options from parameters - ...options ?? {} - }; - } - public static DEBUGGER_MAGIC = 'bsdebug'; // 64-bit = [b'bsdebug\0' little-endian] - - public scriptTitle: string; - public handshakeComplete = false; - public connectedToIoPort = false; - public watchPacketLength = false; - public protocolVersion: string; - public primaryThread: number; - public stackFrameIndex: number; - - private emitter = new EventEmitter(); - private controllerClient: Net.Socket; - private ioClient: Net.Socket; - private unhandledData: Buffer; - private stopped = false; - private totalRequests = 0; - private activeRequests = {}; - private options: ConstructorOptions; - - /** - * Prior to protocol v3.1.0, the Roku device would regularly set the wrong thread as "active", - * so this flag lets us know if we should use our better-than-nothing workaround - */ - private get enableThreadHoppingWorkaround() { - return semver.satisfies(this.protocolVersion, '<3.1.0'); - } - - /** - * Starting in protocol v3.1.0, component libary breakpoints must be added in the format `lib://`, but prior they didn't require this. - * So this flag tells us which format to support - */ - private get enableComponentLibrarySpecificBreakpoints() { - return semver.satisfies(this.protocolVersion, '>=3.1.0'); - } - - /** - * Starting in protocol v3.1.0, breakpoints can support conditional expressions. This flag indicates whether the current sessuion supports that functionality. - */ - private get supportsConditionalBreakpoints() { - return semver.satisfies(this.protocolVersion, '>=3.1.0'); - } - - public get supportsBreakpointRegistrationWhileRunning() { - return semver.satisfies(this.protocolVersion, '>=3.2.0'); - } - - public get supportsBreakpointVerification() { - return semver.satisfies(this.protocolVersion, '>=3.2.0'); - } - - /** - * Get a promise that resolves after an event occurs exactly once - */ - public once(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'start'): Promise; - public once(eventName: 'breakpoints-verified'): Promise; - public once(eventName: 'data'): Promise; - public once(eventName: 'runtime-error' | 'suspend'): Promise; - public once(eventName: 'connected'): Promise; - public once(eventName: 'io-output'): Promise; - public once(eventName: 'protocol-version'): Promise; - public once(eventName: 'handshake-verified'): Promise; - public once(eventName: string) { - return new Promise((resolve) => { - const disconnect = this.on(eventName as Parameters[0], (...args) => { - disconnect(); - resolve(...args); - }); - }); - } - - /** - * Subscribe to various events - */ - public on(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'start', handler: () => void); - public on(eventName: 'breakpoints-verified', handler: (data: VerifiedBreakpointsData) => void); - public on(eventName: 'data', handler: (data: any) => void); - public on(eventName: 'runtime-error' | 'suspend', handler: (data: UpdateThreadsResponse) => void); - public on(eventName: 'connected', handler: (connected: boolean) => void); - public on(eventName: 'io-output', handler: (output: string) => void); - public on(eventName: 'protocol-version', handler: (data: ProtocolVersionDetails) => void); - public on(eventName: 'handshake-verified', handler: (data: HandshakeResponse) => void); - // public on(eventname: 'rendezvous', handler: (output: RendezvousHistory) => void); - // public on(eventName: 'runtime-error', handler: (error: BrightScriptRuntimeError) => void); - public on(eventName: string, handler: (payload: any) => void) { - this.emitter.on(eventName, handler); - return () => { - this.emitter?.removeListener(eventName, handler); - }; - } - - private emit(eventName: 'suspend' | 'runtime-error', data: UpdateThreadsResponse); - private emit(eventName: 'breakpoints-verified', data: VerifiedBreakpointsData); - private emit(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'connected' | 'data' | 'handshake-verified' | 'io-output' | 'protocol-version' | 'start', data?); - private emit(eventName: string, data?) { - //emit these events on next tick, otherwise they will be processed immediately which could cause issues - setTimeout(() => { - //in rare cases, this event is fired after the debugger has closed, so make sure the event emitter still exists - this.emitter?.emit(eventName, data); - }, 0); - } - - private async establishControllerConnection() { - const pendingSockets = new Set(); - const connection = await new Promise((resolve) => { - util.setInterval((cancelInterval) => { - const socket = new Net.Socket(); - pendingSockets.add(socket); - socket.on('error', (error) => { - console.debug(Date.now(), 'Encountered an error connecting to the debug protocol socket. Ignoring and will try again soon', error); - }); - socket.connect({ port: this.options.controllerPort, host: this.options.host }, () => { - cancelInterval(); - - this.logger.debug(`Connected to debug protocol controller port. Socket ${[...pendingSockets].indexOf(socket)} of ${pendingSockets.size} was the winner`); - //clean up all remaining pending sockets - for (const pendingSocket of pendingSockets) { - pendingSocket.removeAllListeners(); - //cleanup and destroy all other sockets - if (pendingSocket !== socket) { - pendingSocket.end(); - pendingSocket?.destroy(); - } - } - pendingSockets.clear(); - resolve(socket); - }); - }, this.options.controllerConnectInterval ?? 250); - }); - return connection; - } - - public async connect(): Promise { - this.logger.log('connect', this.options); - - // If there is no error, the server has accepted the request and created a new dedicated control socket - this.controllerClient = await this.establishControllerConnection(); - - this.controllerClient.on('data', (buffer) => { - this.writeToBufferLog('server-to-client', buffer); - if (this.unhandledData) { - this.unhandledData = Buffer.concat([this.unhandledData, buffer]); - } else { - this.unhandledData = buffer; - } - - this.logger.debug(`on('data'): incoming bytes`, buffer.length, buffer.toJSON()); - const startBufferSize = this.unhandledData.length; - - this.parseUnhandledData(this.unhandledData); - - const endBufferSize = this.unhandledData?.length ?? 0; - this.logger.debug(`buffer size before:`, startBufferSize, ', buffer size after:', endBufferSize, ', bytes consumed:', startBufferSize - endBufferSize); - }); - - this.controllerClient.on('end', () => { - this.logger.log('TCP connection closed'); - this.shutdown('app-exit'); - }); - - // Don't forget to catch error, for your own sake. - this.controllerClient.once('error', (error) => { - //the Roku closed the connection for some unknown reason... - console.error(`TCP connection error on control port`, error); - this.shutdown('close'); - }); - - //send the magic, which triggers the debug session - this.sendMagic(); - - //wait for the handshake response from the device - const isConnected = await this.once('connected'); - return isConnected; - } - - private sendMagic() { - let buffer = new SmartBuffer({ size: Buffer.byteLength(Debugger.DEBUGGER_MAGIC) + 1 }).writeStringNT(Debugger.DEBUGGER_MAGIC).toBuffer(); - this.logger.log('Sending magic to server'); - this.writeToBufferLog('client-to-server', buffer); - this.controllerClient.write(buffer); - } - - /** - * Write a specific buffer log entry to the logger, which, when file logging is enabled - * can be extracted and processed through the DebugProtocolClientReplaySession - */ - private writeToBufferLog(type: 'server-to-client' | 'client-to-server' | 'io', buffer: Buffer) { - this.logger.log('[[bufferLog]]:', JSON.stringify({ - type: type, - timestamp: new Date().toISOString(), - buffer: buffer.toJSON() - })); - } - - - public async continue() { - if (this.stopped) { - this.stopped = false; - return this.makeRequest(new SmartBuffer({ size: 12 }), COMMANDS.CONTINUE); - } - } - - public async pause(force = false) { - if (!this.stopped || force) { - return this.makeRequest(new SmartBuffer({ size: 12 }), COMMANDS.STOP); - } - } - - public async exitChannel() { - return this.makeRequest(new SmartBuffer({ size: 12 }), COMMANDS.EXIT_CHANNEL); - } - - public async stepIn(threadId: number = this.primaryThread) { - return this.step(STEP_TYPE.STEP_TYPE_LINE, threadId); - } - - public async stepOver(threadId: number = this.primaryThread) { - return this.step(STEP_TYPE.STEP_TYPE_OVER, threadId); - } - - public async stepOut(threadId: number = this.primaryThread) { - return this.step(STEP_TYPE.STEP_TYPE_OUT, threadId); - } - - private async step(stepType: STEP_TYPE, threadId: number): Promise { - this.logger.log('[step]', { stepType: STEP_TYPE[stepType], threadId, stopped: this.stopped }); - let buffer = new SmartBuffer({ size: 17 }); - buffer.writeUInt32LE(threadId); // thread_index - buffer.writeUInt8(stepType); // step_type - if (this.stopped) { - this.stopped = false; - let stepResult = await this.makeRequest(buffer, COMMANDS.STEP); - if (stepResult.errorCode === ERROR_CODES.OK) { - // this.stopped = true; - // this.emit('suspend'); - } else { - // there is a CANT_CONTINUE error code but we can likely treat all errors like a CANT_CONTINUE - this.emit('cannot-continue'); - } - return stepResult; - } - } - - public async threads() { - if (this.stopped) { - let result = await this.makeRequest(new SmartBuffer({ size: 12 }), COMMANDS.THREADS); - - if (result.errorCode === ERROR_CODES.OK) { - //older versions of the debug protocol had issues with maintaining the active thread, so our workaround is to keep track of it elsewhere - if (this.enableThreadHoppingWorkaround) { - //ignore the `isPrimary` flag on threads - this.logger.debug(`Ignoring the 'isPrimary' flag from threads because protocol version ${this.protocolVersion} and lower has a bug`); - } else { - //trust the debug protocol's `isPrimary` flag on threads - for (let i = 0; i < result.threadsCount; i++) { - let thread = result.threads[i]; - if (thread.isPrimary) { - this.primaryThread = i; - break; - } - } - } - } - return result; - } - } - - public async stackTrace(threadIndex: number = this.primaryThread) { - let buffer = new SmartBuffer({ size: 16 }); - buffer.writeUInt32LE(threadIndex); // thread_index - if (this.stopped && threadIndex > -1) { - return this.makeRequest(buffer, COMMANDS.STACKTRACE); - } - } - - /** - * @param variablePathEntries One or more path entries to the variable to be inspected. E.g., m.top.myObj["someKey"] can be accessed with ["m","top","myobj","\"someKey\""]. - * - * If no path is specified, the variables accessible from the specified stack frame are returned. - * - * Starting in protocol v3.1.0, The keys for indexed gets (i.e. obj["key"]) should be wrapped in quotes so they can be handled in a case-sensitive fashion (if applicable on device). - * All non-quoted keys (i.e. strings without leading and trailing quotes inside them) will be treated as case-insensitive). - * @param getChildKeys If set, VARIABLES response include the child keys for container types like lists and associative arrays - * @param stackFrameIndex 0 = first function called, nframes-1 = last function. This indexing does not match the order of the frames returned from the STACKTRACE command - * @param threadIndex the index (or perhaps ID?) of the thread to get variables for - */ - public async getVariables(variablePathEntries: Array = [], getChildKeys = true, stackFrameIndex: number = this.stackFrameIndex, threadIndex: number = this.primaryThread) { - if (this.stopped && threadIndex > -1) { - //starting in protocol v3.1.0, it supports marking certain path items as case-insensitive (i.e. parts of DottedGet expressions) - const sendCaseInsensitiveData = semver.satisfies(this.protocolVersion, '>=3.1.0') && variablePathEntries.length > 0; - let buffer = new SmartBuffer({ size: 17 }); - let flags = 0; - if (getChildKeys) { - // eslint-disable-next-line no-bitwise - flags |= VARIABLE_REQUEST_FLAGS.GET_CHILD_KEYS; - } - if (sendCaseInsensitiveData) { - // eslint-disable-next-line no-bitwise - flags |= VARIABLE_REQUEST_FLAGS.CASE_SENSITIVITY_OPTIONS; - } - buffer.writeUInt8(flags); // variable_request_flags - buffer.writeUInt32LE(threadIndex); // thread_index - buffer.writeUInt32LE(stackFrameIndex); // stack_frame_index - buffer.writeUInt32LE(variablePathEntries.length); // variable_path_len - variablePathEntries.forEach(entry => { - if (entry.startsWith('"') && entry.endsWith('"')) { - //remove leading and trailing quotes - entry = entry.substring(1, entry.length - 1); - } - buffer.writeStringNT(entry); // variable_path_entries - optional - }); - if (sendCaseInsensitiveData) { - variablePathEntries.forEach(entry => { - buffer.writeUInt8( - //0 means case SENSITIVE lookup, 1 means case INsensitive lookup - entry.startsWith('"') ? 0 : 1 - ); - }); - } - return this.makeRequest(buffer, COMMANDS.VARIABLES, variablePathEntries); - } - } - - public async executeCommand(sourceCode: string, stackFrameIndex: number = this.stackFrameIndex, threadIndex: number = this.primaryThread) { - if (this.stopped && threadIndex > -1) { - console.log(sourceCode); - let buffer = new SmartBuffer({ size: 8 }); - buffer.writeUInt32LE(threadIndex); // thread_index - buffer.writeUInt32LE(stackFrameIndex); // stack_frame_index - buffer.writeStringNT(sourceCode); // source_code - return this.makeRequest(buffer, COMMANDS.EXECUTE, sourceCode); - } - } - - public async addBreakpoints(breakpoints: Array): Promise { - const { enableComponentLibrarySpecificBreakpoints } = this; - if (breakpoints?.length > 0) { - - const useConditionalBreakpoints = ( - //does this protocol version support conditional breakpoints? - this.supportsConditionalBreakpoints && - //is there at least one conditional breakpoint present? - !!breakpoints.find(x => !!x?.conditionalExpression?.trim()) - ); - - let buffer = new SmartBuffer(); - //set the `FLAGS` value if supported - if (useConditionalBreakpoints) { - buffer.writeUInt32LE(0); // flags - Should always be passed as 0. Unused, reserved for future use. - } - buffer.writeUInt32LE(breakpoints.length); // num_breakpoints - The number of breakpoints in the breakpoints array. - for (const breakpoint of breakpoints) { - let { filePath } = breakpoint; - //protocol >= v3.1.0 requires complib breakpoints have a special prefix - if (enableComponentLibrarySpecificBreakpoints) { - if (breakpoint.componentLibraryName) { - filePath = filePath.replace(/^pkg:\//i, `lib:/${breakpoint.componentLibraryName}/`); - } - } - - buffer.writeStringNT(filePath); // file_path - The path of the source file where the breakpoint is to be inserted. - buffer.writeUInt32LE(breakpoint.lineNumber); // line_number - The line number in the channel application code where the breakpoint is to be executed. - buffer.writeUInt32LE(breakpoint.hitCount ?? 0); // ignore_count - The number of times to ignore the breakpoint condition before executing the breakpoint. This number is decremented each time the channel application reaches the breakpoint. - //if the protocol supports conditional breakpoints, add any present condition - if (useConditionalBreakpoints) { - //There's a bug in 3.1 where empty conditional expressions would crash the breakpoints, so just default to `true` which always succeeds - buffer.writeStringNT(breakpoint.conditionalExpression ?? 'true'); // cond_expr - the condition that must evaluate to `true` in order to hit the breakpoint - } - } - const response = await this.makeRequest(buffer, - useConditionalBreakpoints ? COMMANDS.ADD_CONDITIONAL_BREAKPOINTS : COMMANDS.ADD_BREAKPOINTS - ); - //if the device does not support breakpoint verification, then auto-mark all of these as verified - if (!this.supportsBreakpointVerification) { - this.emit('breakpoints-verified', response); - } - return response; - } - const response = new AddBreakpointsResponse(null); - response.success = true; - response.errorCode = ERROR_CODES.OK; - return response; - } - - public async listBreakpoints(): Promise { - return this.makeRequest(new SmartBuffer({ size: 12 }), COMMANDS.LIST_BREAKPOINTS); - } - - public async removeBreakpoints(breakpointIds: number[]): Promise { - if (breakpointIds?.length > 0) { - let buffer = new SmartBuffer(); - buffer.writeUInt32LE(breakpointIds.length); // num_breakpoints - The number of breakpoints in the breakpoints array. - breakpointIds.forEach((breakpointId) => { - buffer.writeUInt32LE(breakpointId); // breakpoint_ids - An array of breakpoint IDs representing the breakpoints to be removed. - }); - return this.makeRequest(buffer, COMMANDS.REMOVE_BREAKPOINTS); - } - return new RemoveBreakpointsResponse(null); - } - - private async makeRequest(buffer: SmartBuffer, command: COMMANDS, extraData?) { - this.totalRequests++; - let requestId = this.totalRequests; - buffer.insertUInt32LE(command, 0); // command_code - An enum representing the debugging command being sent. See the COMMANDS enum - buffer.insertUInt32LE(requestId, 0); // request_id - The ID of the debugger request (must be >=1). This ID is included in the debugger response. - buffer.insertUInt32LE(buffer.writeOffset + 4, 0); // packet_length - The size of the packet to be sent. - - this.activeRequests[requestId] = { - commandType: command, - commandTypeText: COMMANDS[command], - extraData: extraData - }; - - return new Promise((resolve, reject) => { - let unsubscribe = this.on('data', (data) => { - if (data.requestId === requestId) { - unsubscribe(); - resolve(data as T); - } - }); - - this.logger.debug('makeRequest', `requestId=${requestId}`, this.activeRequests[requestId]); - if (this.controllerClient) { - const buff = buffer.toBuffer(); - this.writeToBufferLog('client-to-server', buff); - this.controllerClient.write(buff); - } else { - throw new Error(`Controller connection was closed - Command: ${COMMANDS[command]}`); - } - }); - } - - private parseUnhandledData(buffer: Buffer): boolean { - if (buffer.length < 1) { - // short circuit if the buffer is empty - return false; - } - - if (this.handshakeComplete) { - let debuggerRequestResponse = this.watchPacketLength ? new ProtocolEventV3(buffer) : new ProtocolEvent(buffer); - let packetLength = debuggerRequestResponse.packetLength; - let slicedBuffer = packetLength ? buffer.slice(4) : buffer; - - this.logger.log(`incoming bytes: ${buffer.length}`, debuggerRequestResponse, slicedBuffer.toJSON()); - if (debuggerRequestResponse.success) { - if (debuggerRequestResponse.requestId > this.totalRequests) { - this.removedProcessedBytes(debuggerRequestResponse, slicedBuffer, packetLength); - return true; - } - - if (debuggerRequestResponse.errorCode !== ERROR_CODES.OK) { - this.logger.error(debuggerRequestResponse.errorCode, debuggerRequestResponse); - this.removedProcessedBytes(debuggerRequestResponse, buffer, packetLength); - return true; - } - - if (debuggerRequestResponse.updateType > 0) { - this.logger.log('Update Type:', UPDATE_TYPES[debuggerRequestResponse.updateType]); - switch (debuggerRequestResponse.updateType) { - case UPDATE_TYPES.IO_PORT_OPENED: - return this.connectToIoPort(new ConnectIOPortResponse(slicedBuffer), buffer, packetLength); - case UPDATE_TYPES.ALL_THREADS_STOPPED: - case UPDATE_TYPES.THREAD_ATTACHED: - let debuggerUpdateThreads = new UpdateThreadsResponse(slicedBuffer); - this.logger.log(debuggerUpdateThreads); - if (debuggerUpdateThreads.success) { - this.handleThreadsUpdate(debuggerUpdateThreads); - this.removedProcessedBytes(debuggerUpdateThreads, slicedBuffer, packetLength); - return true; - } - return false; - case UPDATE_TYPES.UNDEF: - return this.checkResponse(new UndefinedResponse(slicedBuffer), buffer, packetLength); - case UPDATE_TYPES.BREAKPOINT_ERROR: - const response = new BreakpointErrorUpdateResponse(slicedBuffer); - //we do nothing with breakpoint errors at this time. - return this.checkResponse(response, buffer, packetLength); - case UPDATE_TYPES.COMPILE_ERROR: - return this.checkResponse(new UndefinedResponse(slicedBuffer), buffer, packetLength); - case UPDATE_TYPES.BREAKPOINT_VERIFIED: - const bpVerifiedResponse = new BreakpointVerifiedUpdateResponse(slicedBuffer); - console.log('breakpoints verified', bpVerifiedResponse.breakpoints.map(x => x.breakpointId)); - if (this.checkResponse(bpVerifiedResponse, buffer, packetLength)) { - this.emit('breakpoints-verified', { - breakpoints: bpVerifiedResponse.breakpoints - }); - return true; - } else { - return false; - } - default: - return this.checkResponse(new UndefinedResponse(slicedBuffer), buffer, packetLength); - } - } else { - this.logger.log('Command Type:', COMMANDS[this.activeRequests[debuggerRequestResponse.requestId].commandType]); - switch (this.activeRequests[debuggerRequestResponse.requestId].commandType) { - case COMMANDS.STOP: - case COMMANDS.CONTINUE: - case COMMANDS.STEP: - case COMMANDS.EXIT_CHANNEL: - this.removedProcessedBytes(debuggerRequestResponse, buffer, packetLength); - return true; - case COMMANDS.EXECUTE: - return this.checkResponse(new ExecuteResponseV3(slicedBuffer), buffer, packetLength); - case COMMANDS.ADD_BREAKPOINTS: - case COMMANDS.ADD_CONDITIONAL_BREAKPOINTS: - return this.checkResponse(new AddBreakpointsResponse(slicedBuffer), buffer, packetLength); - case COMMANDS.LIST_BREAKPOINTS: - return this.checkResponse(new ListBreakpointsResponse(slicedBuffer), buffer, packetLength); - case COMMANDS.REMOVE_BREAKPOINTS: - return this.checkResponse(new RemoveBreakpointsResponse(slicedBuffer), buffer, packetLength); - case COMMANDS.VARIABLES: - return this.checkResponse(new VariableResponse(slicedBuffer), buffer, packetLength); - case COMMANDS.STACKTRACE: - return this.checkResponse( - packetLength ? new StackTraceResponseV3(slicedBuffer) : new StackTraceResponse(slicedBuffer), - buffer, - packetLength); - case COMMANDS.THREADS: - return this.checkResponse(new ThreadsResponse(slicedBuffer), buffer, packetLength); - default: - return this.checkResponse(debuggerRequestResponse, buffer, packetLength); - } - } - } - } else { - let debuggerHandshake: HandshakeResponse | HandshakeResponseV3; - debuggerHandshake = new HandshakeResponseV3(buffer); - this.logger.log(`incoming bytes: ${buffer.length}`, debuggerHandshake); - - if (!debuggerHandshake.success) { - debuggerHandshake = new HandshakeResponse(buffer); - } - - if (debuggerHandshake.success) { - this.handshakeComplete = true; - this.verifyHandshake(debuggerHandshake); - this.removedProcessedBytes(debuggerHandshake, buffer); - //once the handshake is complete, we have successfully "connected" - this.emit('connected', true); - return true; - } - } - - return false; - } - - private checkResponse(responseClass: { requestId: number; readOffset: number; success: boolean }, unhandledData: Buffer, packetLength = 0) { - if (responseClass.success) { - this.removedProcessedBytes(responseClass, unhandledData, packetLength); - return true; - } else if (packetLength > 0 && unhandledData.length >= packetLength) { - this.removedProcessedBytes(responseClass, unhandledData, packetLength); - } - return false; - } - - private removedProcessedBytes(responseHandler: { requestId: number; readOffset: number }, unhandledData: Buffer, packetLength = 0) { - const activeRequest = this.activeRequests[responseHandler.requestId]; - if (responseHandler.requestId > 0 && this.activeRequests[responseHandler.requestId]) { - delete this.activeRequests[responseHandler.requestId]; - } - - this.emit('data', responseHandler); - - this.unhandledData = unhandledData.slice(packetLength ? packetLength : responseHandler.readOffset); - this.logger.debug('[raw]', `requestId=${responseHandler?.requestId}`, activeRequest, (responseHandler as any)?.constructor?.name ?? '', responseHandler); - this.parseUnhandledData(this.unhandledData); - } - - private verifyHandshake(debuggerHandshake: HandshakeResponse): boolean { - const magicIsValid = (Debugger.DEBUGGER_MAGIC === debuggerHandshake.magic); - if (magicIsValid) { - this.logger.log('Magic is valid.'); - this.protocolVersion = [debuggerHandshake.majorVersion, debuggerHandshake.minorVersion, debuggerHandshake.patchVersion].join('.'); - this.logger.log('Protocol Version:', this.protocolVersion); - - this.watchPacketLength = debuggerHandshake.watchPacketLength; - - let handshakeVerified = true; - - if (semver.satisfies(this.protocolVersion, this.supportedVersionRange)) { - this.logger.log('supported'); - this.emit('protocol-version', { - message: `Protocol Version ${this.protocolVersion} is supported!`, - errorCode: PROTOCOL_ERROR_CODES.SUPPORTED - }); - } else if (semver.gtr(this.protocolVersion, this.supportedVersionRange)) { - this.logger.log('roku-debug has not been tested against protocol version', this.protocolVersion); - this.emit('protocol-version', { - message: `Protocol Version ${this.protocolVersion} has not been tested and my not work as intended.\nPlease open any issues you have with this version to https://github.com/rokucommunity/roku-debug/issues`, - errorCode: PROTOCOL_ERROR_CODES.NOT_TESTED - }); - } else { - this.logger.log('not supported'); - this.emit('protocol-version', { - message: `Protocol Version ${this.protocolVersion} is not supported.\nIf you believe this is an error please open an issue at https://github.com/rokucommunity/roku-debug/issues`, - errorCode: PROTOCOL_ERROR_CODES.NOT_SUPPORTED - }); - this.shutdown('close'); - handshakeVerified = false; - } - - this.emit('handshake-verified', handshakeVerified); - return handshakeVerified; - } else { - this.logger.log('Closing connection due to bad debugger magic', debuggerHandshake.magic); - this.emit('handshake-verified', false); - this.shutdown('close'); - return false; - } - } - - private connectToIoPort(connectIoPortResponse: ConnectIOPortResponse, unhandledData: Buffer, packetLength = 0) { - this.logger.log('Connecting to IO port. response status success =', connectIoPortResponse.success); - if (connectIoPortResponse.success) { - // Create a new TCP client. - this.ioClient = new Net.Socket(); - // Send a connection request to the server. - this.logger.log('Connect to IO Port: port', connectIoPortResponse.data, 'host', this.options.host); - this.ioClient.connect({ port: connectIoPortResponse.data, host: this.options.host }, () => { - // If there is no error, the server has accepted the request - this.logger.log('TCP connection established with the IO Port.'); - this.connectedToIoPort = true; - - let lastPartialLine = ''; - this.ioClient.on('data', (buffer) => { - this.writeToBufferLog('io', buffer); - let responseText = buffer.toString(); - if (!responseText.endsWith('\n')) { - // buffer was split, save the partial line - lastPartialLine += responseText; - } else { - if (lastPartialLine) { - // there was leftover lines, join the partial lines back together - responseText = lastPartialLine + responseText; - lastPartialLine = ''; - } - // Emit the completed io string. - this.emit('io-output', responseText.trim()); - } - }); - - this.ioClient.on('end', () => { - this.ioClient.end(); - this.logger.log('Requested an end to the IO connection'); - }); - - // Don't forget to catch error, for your own sake. - this.ioClient.once('error', (err) => { - this.ioClient.end(); - this.logger.error(err); - }); - }); - - this.removedProcessedBytes(connectIoPortResponse, unhandledData, packetLength); - return true; - } - return false; - } - - private handleThreadsUpdate(update: UpdateThreadsResponse) { - this.stopped = true; - let stopReason = update.data.stopReason; - let eventName: 'runtime-error' | 'suspend' = stopReason === STOP_REASONS.RUNTIME_ERROR ? 'runtime-error' : 'suspend'; - - if (update.updateType === UPDATE_TYPES.ALL_THREADS_STOPPED) { - if (stopReason === STOP_REASONS.RUNTIME_ERROR || stopReason === STOP_REASONS.BREAK || stopReason === STOP_REASONS.STOP_STATEMENT) { - this.primaryThread = (update.data as ThreadsStopped).primaryThreadIndex; - this.stackFrameIndex = 0; - this.emit(eventName, update); - } - } else if (stopReason === STOP_REASONS.RUNTIME_ERROR || stopReason === STOP_REASONS.BREAK || stopReason === STOP_REASONS.STOP_STATEMENT) { - this.primaryThread = (update.data as ThreadAttached).threadIndex; - this.emit(eventName, update); - } - } - - public destroy() { - this.shutdown('close'); - } - - private shutdown(eventName: 'app-exit' | 'close') { - if (this.controllerClient) { - this.controllerClient.removeAllListeners(); - this.controllerClient.destroy(); - this.controllerClient = undefined; - } - - if (this.ioClient) { - this.ioClient.removeAllListeners(); - this.ioClient.destroy(); - this.ioClient = undefined; - } - - this.emit(eventName); - } -} - -export interface ProtocolVersionDetails { - message: string; - errorCode: PROTOCOL_ERROR_CODES; -} - -export interface BreakpointSpec { - /** - * The path of the source file where the breakpoint is to be inserted. - */ - filePath: string; - /** - * The (1-based) line number in the channel application code where the breakpoint is to be executed. - */ - lineNumber: number; - /** - * The number of times to ignore the breakpoint condition before executing the breakpoint. This number is decremented each time the channel application reaches the breakpoint. - */ - hitCount?: number; - /** - * BrightScript code that evaluates to a boolean value. The expression is compiled and executed in - * the context where the breakpoint is located. If specified, the hitCount is only be - * updated if this evaluates to true. - * @avaiable since protocol version 3.1.0 - */ - conditionalExpression?: string; -} - -export interface ConstructorOptions { - /** - * The host/ip address of the Roku - */ - host: string; - /** - * The port number used to send all debugger commands. This is static/unchanging for Roku devices, - * but is configurable here to support unit testing or alternate runtimes (i.e. https://www.npmjs.com/package/brs) - */ - controllerPort?: number; - /** - * The interval (in milliseconds) for how frequently the `connect` - * call should retry connecting to the controller port. At the start of a debug session, - * the protocol debugger will start trying to connect the moment the channel is sideloaded, - * and keep trying until a successful connection is established or the debug session is terminated - * @default 250 - */ - controllerConnectInterval?: number; - /** - * The maximum time (in milliseconds) the debugger will keep retrying connections. - * This is here to prevent infinitely pinging the Roku device. - */ - controllerConnectMaxTime?: number; -} diff --git a/src/debugProtocol/MockDebugProtocolServer.spec.ts b/src/debugProtocol/MockDebugProtocolServer.spec.ts deleted file mode 100644 index 1339ad26..00000000 --- a/src/debugProtocol/MockDebugProtocolServer.spec.ts +++ /dev/null @@ -1,173 +0,0 @@ -import * as net from 'net'; -import type { Subscription } from 'rxjs'; -import { ReplaySubject } from 'rxjs'; -import { SmartBuffer } from 'smart-buffer'; -import type { Deferred } from '../util'; -import { util, defer } from '../util'; - -export class MockDebugProtocolServer { - /** - * The net server that will be listening for incoming socket connections from clients - */ - public server: net.Server; - /** - * The list of server sockets created in response to clients connecting. - * There should be one for every client - */ - public client: Client; - - /** - * The port that the client should use to send commands - */ - public controllerPort: number; - - public actions = [] as Action[]; - - private clientLoadedPromise: Promise; - - private processActionsSubscription: Subscription; - - public async initialize() { - const clientDeferred = defer(); - this.clientLoadedPromise = clientDeferred.promise; - void new Promise((resolve) => { - this.server = net.createServer((s) => { - this.client = new Client(s); - clientDeferred.resolve(); - }); - }); - this.server.listen(0); - //wait for the server to start listening - await new Promise((resolve) => { - this.server.on('listening', () => { - this.controllerPort = (this.server.address() as net.AddressInfo).port; - resolve(); - }); - }); - } - - /** - * After queueing up actions, this method starts processing those actions. - * If an action cannot be processed yet, it will wait until the client sends the corresponding - * request. If that request never comes, this server will wait indefinitely - */ - public async processActions() { - //wait for a client to connect - await this.clientLoadedPromise; - - //listen to all events sent to the client - console.log('subscription being created'); - // eslint-disable-next-line @typescript-eslint/no-misused-promises - this.processActionsSubscription = this.client.subject.subscribe(async () => { - console.log('subscription handler fired'); - //process events until one of them returns false. - //when an event returns false, we will wait for more data to come back and try again - while (await this.actions[0]?.process(this.client) === true) { - this.actions.splice(0, 1); - } - }); - } - - public waitForMagic() { - const action = new WaitForMagicAction(); - this.actions.push(action); - return action; - } - - public sendHandshakeResponse(magic: Promise | string) { - const action = new SendHandshakeResponseAction(magic); - this.actions.push(action); - return action; - } - - public reset() { - this.client?.destroy(); - this.client = undefined; - this.processActionsSubscription.unsubscribe(); - this.actions = []; - } - - public destroy() { - this.server?.close(); - this.server = undefined; - } -} - -class Client { - constructor( - public socket: net.Socket - ) { - const handler = (data) => { - this.buffer = Buffer.concat([this.buffer, data]); - this.subject.next(undefined); - }; - socket.on('data', handler); - this.disconnectSocket = () => { - this.socket.off('data', handler); - }; - } - public subject = new ReplaySubject(); - public buffer = Buffer.alloc(0); - public disconnectSocket: () => void; - - public destroy() { - this.disconnectSocket(); - this.subject.complete(); - this.socket.destroy(); - } -} - -abstract class Action { - constructor() { - this.deferred = defer(); - } - protected deferred: Deferred; - public get promise() { - return this.deferred.promise; - } - /** - * - * @param ref - an object that has a property named "buffer". This is so that, if new data comes in, - * the client can update the reference to the buffer, and the actions can alter that new buffer directly - */ - public abstract process(client: Client): Promise; -} - -class WaitForMagicAction extends Action { - public process(client: Client) { - const b = SmartBuffer.fromBuffer(client.buffer); - try { - const str = util.readStringNT(b); - this.deferred.resolve(str); - client.buffer = client.buffer.slice(b.readOffset); - return Promise.resolve(true); - } catch (e) { - console.error('WaitForMagicAction failed', e); - return Promise.resolve(false); - } - } -} - -class SendHandshakeResponseAction extends Action { - constructor( - private magic: Promise | string - ) { - super(); - } - - public async process(client: Client) { - console.log('processing handshake response'); - const magic = await Promise.resolve(this.magic); - const b = new SmartBuffer(); - b.writeStringNT(magic); - b.writeInt32LE(2); - b.writeInt32LE(0); - b.writeInt32LE(0); - const buffer = b.toBuffer(); - - client.socket.write(buffer); - this.deferred.resolve(); - console.log('sent handshake response'); - return true; - } -} diff --git a/src/debugProtocol/PluginInterface.ts b/src/debugProtocol/PluginInterface.ts new file mode 100644 index 00000000..248543ab --- /dev/null +++ b/src/debugProtocol/PluginInterface.ts @@ -0,0 +1,103 @@ +// inspiration: https://github.com/andywer/typed-emitter/blob/master/index.d.ts +export type Arguments = [T] extends [(...args: infer U) => any] + ? U + : [T] extends [void] ? [] : [T]; + +export default class PluginInterface { + constructor( + plugins = [] as TPlugin[] + ) { + for (const plugin of plugins ?? []) { + this.add(plugin); + } + } + + private plugins: Array> = []; + + /** + * Call `event` on plugins + */ + public async emit(eventName: K, event: Arguments[0]) { + for (let { plugin } of this.plugins) { + if ((plugin as any)[eventName]) { + await Promise.resolve((plugin as any)[eventName](event)); + } + } + return event; + } + + /** + * Add a plugin to the end of the list of plugins + * @param plugin the plugin + * @param priority the priority for the plugin. Lower number means higher priority. (ex: 1 executes before 5) + */ + public add(plugin: T, priority = 1) { + const container = { + plugin: plugin, + priority: priority + }; + this.plugins.push(container); + + //sort the plugins by priority + this.plugins.sort((a, b) => { + return a.priority - b.priority; + }); + + return plugin; + } + + /** + * Adds a temporary plugin with a single event hook, and resolve a promise with the event from the next occurance of that event. + * Once the event fires for the first time, the plugin is unregistered. + * @param eventName the name of the event to subscribe to + * @param priority the priority for this event. Lower number means higher priority. (ex: 1 executes before 5) + */ + public once(eventName: keyof TPlugin, priority = 1): Promise { + return this.onceIf(eventName, () => true, priority); + } + + /** + * Adds a temporary plugin with a single event hook, and resolve a promise with the event from the next occurance of that event. + * Once the event fires for the first time and the matcher evaluates to true, the plugin is unregistered. + * @param eventName the name of the event to subscribe to + * @param matcher a function to call that, when true, will deregister this hander and return the event + * @param priority the priority for this event. Lower number means higher priority. (ex: 1 executes before 5) + */ + public onceIf(eventName: keyof TPlugin, matcher: (TEventType) => boolean, priority = 1): Promise { + return new Promise((resolve) => { + const tempPlugin = {} as any; + tempPlugin[eventName] = (event) => { + if (matcher(event)) { + //remove the temp plugin + this.remove(tempPlugin); + //resolve the promise with this event + resolve(event); + } + }; + this.add(tempPlugin, priority); + }) as any; + } + + /** + * Remove the specified plugin + */ + public remove(plugin: T) { + for (let i = this.plugins.length - 1; i >= 0; i--) { + if (this.plugins[i].plugin === plugin) { + this.plugins.splice(i, 1); + } + } + } + + /** + * Remove all plugins + */ + public clear() { + this.plugins = []; + } +} + +interface PluginContainer { + plugin: TPlugin; + priority: number; +} diff --git a/src/debugProtocol/ProtocolUtil.spec.ts b/src/debugProtocol/ProtocolUtil.spec.ts new file mode 100644 index 00000000..964fe563 --- /dev/null +++ b/src/debugProtocol/ProtocolUtil.spec.ts @@ -0,0 +1,55 @@ +import { expect } from 'chai'; +import { protocolUtil } from './ProtocolUtil'; +import type { ProtocolUpdate } from './events/ProtocolEvent'; +import { SmartBuffer } from 'smart-buffer'; +import { ErrorCode, UpdateType, UpdateTypeCode } from './Constants'; +import { expectThrows } from '../testHelpers.spec'; + +describe('ProtocolUtil', () => { + describe('loadJson', () => { + it('defaults to an empty object', () => { + protocolUtil.loadJson({} as any, undefined); + //test passes if there was no exception + }); + }); + + describe('bufferLoaderHelper', () => { + it('handles when no event success', () => { + expect( + protocolUtil.bufferLoaderHelper({ + readOffset: -1 + } as any, Buffer.alloc(1), 0, () => false).readOffset + ).to.eql(-1); + }); + }); + + describe('loadCommonUpdateFields', () => { + it('handles when the requestId is greater than 0', () => { + const update = { + data: {} + } as ProtocolUpdate; + const buffer = new SmartBuffer(); + buffer.writeUInt32LE(12); //packet_length + buffer.writeUInt32LE(999); //request_id + buffer.writeUInt32LE(ErrorCode.OK); //error_code + expectThrows( + () => protocolUtil.loadCommonUpdateFields(update, buffer, UpdateType.CompileError), + 'This is not an update' + ); + }); + + it('returns false if this is the wrong update type', () => { + const update = { + data: {} + } as ProtocolUpdate; + const buffer = new SmartBuffer(); + buffer.writeUInt32LE(12); //packet_length + buffer.writeUInt32LE(0); //request_id + buffer.writeUInt32LE(ErrorCode.OK); //error_code + buffer.writeUInt32LE(UpdateTypeCode.AllThreadsStopped); //update_type + expect( + protocolUtil.loadCommonUpdateFields(update, buffer, UpdateType.CompileError) + ).to.be.false; + }); + }); +}); diff --git a/src/debugProtocol/ProtocolUtil.ts b/src/debugProtocol/ProtocolUtil.ts new file mode 100644 index 00000000..a7b81cca --- /dev/null +++ b/src/debugProtocol/ProtocolUtil.ts @@ -0,0 +1,181 @@ +import { SmartBuffer } from 'smart-buffer'; +import { util } from '../util'; +import type { Command, UpdateType } from './Constants'; +import { CommandCode, UpdateTypeCode, ErrorCode, ErrorFlags } from './Constants'; +import type { ProtocolRequest, ProtocolResponse, ProtocolUpdate } from './events/ProtocolEvent'; + +export class ProtocolUtil { + + /** + * Load json data onto an event, and mark it as successful + */ + public loadJson(event: { data: any; success: boolean }, data: any) { + event.data = { + ...event.data, + ...(data ?? {}) + }; + event.success = true; + } + + /** + * Helper function for buffer loading. + * Handles things like try/catch, setting buffer read offset, etc + */ + public bufferLoaderHelper(event: { success: boolean; readOffset: number; data?: { packetLength?: number } }, buffer: Buffer, minByteLength: number, processor: (buffer: SmartBuffer) => boolean | void) { + // Required size of this processor + try { + if (buffer.byteLength >= minByteLength) { + let smartBuffer = SmartBuffer.fromBuffer(buffer); + + //have the processor consume the requred bytes. + event.success = (processor(smartBuffer) ?? true) as boolean; + + //if the event has a packet length, use THAT as the read offset. Otherwise, set the offset to the end of the read position of the buffer + if (event.success) { + if (!event.readOffset) { + event.readOffset = event.data.packetLength ?? smartBuffer.readOffset; + } + } + } + } catch (error) { + // Could not parse + event.readOffset = 0; + event.success = false; + } + return event; + } + + /** + * Load the common DebuggerRequest fields + */ + public loadCommonRequestFields(request: ProtocolRequest, smartBuffer: SmartBuffer) { + request.data.packetLength = smartBuffer.readUInt32LE(); // packet_length + request.data.requestId = smartBuffer.readUInt32LE(); // request_id + request.data.command = CommandCode[smartBuffer.readUInt32LE()] as Command; // command_code + } + + /** + * Load the common DebuggerResponse + */ + public loadCommonResponseFields(response: ProtocolResponse, smartBuffer: SmartBuffer) { + response.data.packetLength = smartBuffer.readUInt32LE(); // packet_length + response.data.requestId = smartBuffer.readUInt32LE(); // request_id + response.data.errorCode = smartBuffer.readUInt32LE(); // error_code + + //if the error code is non-zero, and we have more bytes, then there will be additional data about the error + if (response.data.errorCode !== ErrorCode.OK && response.data.packetLength > smartBuffer.readOffset) { + response.data.errorData = {}; + const errorFlags = smartBuffer.readUInt32LE(); // error_flags + // eslint-disable-next-line no-bitwise + if (errorFlags & ErrorFlags.INVALID_VALUE_IN_PATH) { + response.data.errorData.invalidPathIndex = smartBuffer.readUInt32LE(); // invalid_path_index + } + // eslint-disable-next-line no-bitwise + if (errorFlags & ErrorFlags.MISSING_KEY_IN_PATH) { + response.data.errorData.missingKeyIndex = smartBuffer.readUInt32LE(); // missing_key_index + } + } + } + + public loadCommonUpdateFields(update: ProtocolUpdate, smartBuffer: SmartBuffer, updateType: UpdateType) { + update.data.packetLength = smartBuffer.readUInt32LE(); // packet_length + update.data.requestId = smartBuffer.readUInt32LE(); // request_id + update.data.errorCode = smartBuffer.readUInt32LE(); // error_code + // requestId 0 means this is an update. + if (update.data.requestId === 0) { + update.data.updateType = UpdateTypeCode[smartBuffer.readUInt32LE()] as UpdateType; + + //if this is not the update type we want, return false + if (update.data.updateType !== updateType) { + return false; + } + + } else { + //not an update. We should not proceed any further. + throw new Error('This is not an update'); + } + } + + /** + * Inserts the common command fields to the beginning of the buffer, and computes + * the correct `packet_length` value. + */ + public insertCommonRequestFields(request: ProtocolRequest, smartBuffer: SmartBuffer) { + smartBuffer.insertUInt32LE(CommandCode[request.data.command], 0); // command_code - An enum representing the debugging command being sent. See the COMMANDS enum + smartBuffer.insertUInt32LE(request.data.requestId, 0); // request_id - The ID of the debugger request (must be >=1). This ID is included in the debugger response. + smartBuffer.insertUInt32LE(smartBuffer.writeOffset + 4, 0); // packet_length - The size of the packet to be sent. + request.data.packetLength = smartBuffer.writeOffset; + return smartBuffer; + } + + public insertCommonResponseFields(response: ProtocolResponse, smartBuffer: SmartBuffer) { + //insert error data + const flags = ( + // eslint-disable-next-line no-bitwise + 0 | + (util.isNullish(response?.data?.errorData?.invalidPathIndex) ? 0 : ErrorFlags.INVALID_VALUE_IN_PATH) | + (util.isNullish(response?.data?.errorData?.missingKeyIndex) ? 0 : ErrorFlags.MISSING_KEY_IN_PATH) + ); + if ( + response.data.errorCode !== ErrorCode.OK && + //there's some error data + Object.values(response.data.errorData ?? {}).some(x => !util.isNullish(x)) + ) { + //do these in reverse order since we're writing to the start of the buffer + + if (!util.isNullish(response.data.errorData.missingKeyIndex)) { + smartBuffer.insertUInt32LE(response.data.errorData.missingKeyIndex, 0); + } + //write error data + if (!util.isNullish(response.data.errorData.invalidPathIndex)) { + smartBuffer.insertUInt32LE(response.data.errorData.invalidPathIndex, 0); + } + + //write flags + smartBuffer.insertUInt32LE(flags, 0); + } + smartBuffer.insertUInt32LE(response.data.errorCode, 0); // error_code + smartBuffer.insertUInt32LE(response.data.requestId, 0); // request_id + smartBuffer.insertUInt32LE(smartBuffer.writeOffset + 4, 0); // packet_length + response.data.packetLength = smartBuffer.writeOffset; + return smartBuffer; + } + + + /** + * Inserts the common response fields to the beginning of the buffer, and computes + * the correct `packet_length` value. + */ + public insertCommonUpdateFields(update: ProtocolUpdate, smartBuffer: SmartBuffer) { + smartBuffer.insertUInt32LE(UpdateTypeCode[update.data.updateType], 0); // update_type + smartBuffer.insertUInt32LE(update.data.errorCode, 0); // error_code + smartBuffer.insertUInt32LE(update.data.requestId, 0); // request_id + smartBuffer.insertUInt32LE(smartBuffer.writeOffset + 4, 0); // packet_length + update.data.packetLength = smartBuffer.writeOffset; + return smartBuffer; + } + + /** + * Tries to read a string from the buffer and will throw an error if there is no null terminator. + * @param {SmartBuffer} bufferReader + */ + public readStringNT(bufferReader: SmartBuffer): string { + // Find next null character (if one is not found, throw) + let buffer = bufferReader.toBuffer(); + let foundNullTerminator = false; + for (let i = bufferReader.readOffset; i < buffer.length; i++) { + if (buffer[i] === 0x00) { + foundNullTerminator = true; + break; + } + } + + if (!foundNullTerminator) { + throw new Error('Could not read buffer string as there is no null terminator.'); + } + return bufferReader.readStringNT(); + } +} + +export const protocolUtil = new ProtocolUtil(); + diff --git a/src/debugProtocol/client/DebugProtocolClient.device.spec.ts b/src/debugProtocol/client/DebugProtocolClient.device.spec.ts new file mode 100644 index 00000000..826a31e9 --- /dev/null +++ b/src/debugProtocol/client/DebugProtocolClient.device.spec.ts @@ -0,0 +1,5 @@ +describe('DebugProtocolClient on-device tests', () => { + it('fails for no reason', () => { + throw new Error('Crash!'); + }); +}); diff --git a/src/debugProtocol/client/DebugProtocolClient.spec.ts b/src/debugProtocol/client/DebugProtocolClient.spec.ts new file mode 100644 index 00000000..1a8e6127 --- /dev/null +++ b/src/debugProtocol/client/DebugProtocolClient.spec.ts @@ -0,0 +1,1224 @@ +/* eslint-disable no-bitwise */ +import { DebugProtocolClient } from './DebugProtocolClient'; +import { expect } from 'chai'; +import { createSandbox } from 'sinon'; +import { Command, ErrorCode, StepType, StopReason } from '../Constants'; +import { DebugProtocolServer } from '../server/DebugProtocolServer'; +import { defer, util } from '../../util'; +import { HandshakeRequest } from '../events/requests/HandshakeRequest'; +import { HandshakeResponse } from '../events/responses/HandshakeResponse'; +import type { HandshakeV3Response } from '../events/responses/HandshakeV3Response'; +import { AllThreadsStoppedUpdate } from '../events/updates/AllThreadsStoppedUpdate'; +import type { Variable } from '../events/responses/VariablesResponse'; +import { VariablesResponse, VariableType } from '../events/responses/VariablesResponse'; +import { VariablesRequest } from '../events/requests/VariablesRequest'; +import { DebugProtocolServerTestPlugin } from '../DebugProtocolServerTestPlugin.spec'; +import { ContinueRequest } from '../events/requests/ContinueRequest'; +import { GenericV3Response } from '../events/responses/GenericV3Response'; +import { StopRequest } from '../events/requests/StopRequest'; +import { ExitChannelRequest } from '../events/requests/ExitChannelRequest'; +import { StepRequest } from '../events/requests/StepRequest'; +import type { ThreadInfo } from '../events/responses/ThreadsResponse'; +import { ThreadsResponse } from '../events/responses/ThreadsResponse'; +import { StackTraceResponse } from '../events/responses/StackTraceResponse'; +import { ExecuteRequest } from '../events/requests/ExecuteRequest'; +import { ExecuteV3Response } from '../events/responses/ExecuteV3Response'; +import { AddBreakpointsResponse } from '../events/responses/AddBreakpointsResponse'; +import { AddBreakpointsRequest } from '../events/requests/AddBreakpointsRequest'; +import { AddConditionalBreakpointsRequest } from '../events/requests/AddConditionalBreakpointsRequest'; +import { AddConditionalBreakpointsResponse } from '../events/responses/AddConditionalBreakpointsResponse'; +import { ListBreakpointsRequest } from '../events/requests/ListBreakpointsRequest'; +import { ListBreakpointsResponse } from '../events/responses/ListBreakpointsResponse'; +import { RemoveBreakpointsResponse } from '../events/responses/RemoveBreakpointsResponse'; +import { RemoveBreakpointsRequest } from '../events/requests/RemoveBreakpointsRequest'; +import { expectThrows, expectThrowsAsync } from '../../testHelpers.spec'; +import { StackTraceV3Response } from '../events/responses/StackTraceV3Response'; +import { IOPortOpenedUpdate } from '../events/updates/IOPortOpenedUpdate'; +import * as Net from 'net'; +import { ThreadAttachedUpdate } from '../events/updates/ThreadAttachedUpdate'; +process.on('uncaughtException', (err) => console.log('node js process error\n', err)); +const sinon = createSandbox(); + +describe('DebugProtocolClient', () => { + let server: DebugProtocolServer; + let client: DebugProtocolClient; + let plugin: DebugProtocolServerTestPlugin; + + /** + * Helper function to simplify the initial connect flow + */ + async function connect() { + await client.connect(); + client['options'].shutdownTimeout = 100; + client['options'].exitChannelTimeout = 100; + //send the AllThreadsStopped event, and also wait for the client to suspend + await Promise.all([ + server.sendUpdate(AllThreadsStoppedUpdate.fromJson({ + threadIndex: 2, + stopReason: StopReason.Break, + stopReasonDetail: 'because' + })), + await client.once('suspend') + ]); + } + + beforeEach(async () => { + sinon.stub(console, 'log').callsFake((...args) => { }); + + const options = { + controlPort: undefined as number, + host: '127.0.0.1' + }; + + if (!options.controlPort) { + options.controlPort = await util.getPort(); + } + server = new DebugProtocolServer(options); + plugin = server.plugins.add(new DebugProtocolServerTestPlugin()); + await server.start(); + + client = new DebugProtocolClient(options); + //disable logging for tests because they clutter the test output + client['logger'].logLevel = 'off'; + }); + + afterEach(async () => { + sinon.restore(); + + try { + await client?.destroy(true); + } catch (e) { } + //shut down and destroy the server after each test + try { + await server?.destroy(); + } catch (e) { } + }); + + it('knows when to enable the thread hopping workaround', () => { + //only supported below version 3.1.0 + client.protocolVersion = '1.0.0'; + expect( + client['enableThreadHoppingWorkaround'] + ).to.be.true; + + client.protocolVersion = '3.0.0'; + expect( + client['enableThreadHoppingWorkaround'] + ).to.be.true; + + client.protocolVersion = '3.1.0'; + expect( + client['enableThreadHoppingWorkaround'] + ).to.be.false; + + client.protocolVersion = '4.0.0'; + expect( + client['enableThreadHoppingWorkaround'] + ).to.be.false; + }); + + it('does not crash on unspecified options', () => { + const client = new DebugProtocolClient(undefined); + //no exception means it passed + }); + + it('only sends the continue command when stopped', async () => { + await connect(); + + client.isStopped = false; + await client.continue(); + expect(plugin.latestRequest).not.to.be.instanceof(ContinueRequest); + + plugin.pushResponse(GenericV3Response.fromJson({} as any)); + client.isStopped = true; + await client.continue(); + expect(plugin.latestRequest).to.be.instanceOf(ContinueRequest); + }); + + it('sends the pause command', async () => { + await connect(); + + client.isStopped = true; + await client.pause(); //should do nothing + expect(plugin.latestRequest).not.to.be.instanceof(StopRequest); + + plugin.pushResponse(GenericV3Response.fromJson({} as any)); + client.isStopped = false; + await client.pause(); + expect(plugin.latestRequest).to.be.instanceOf(StopRequest); + }); + + it('sends the exitChannel command', async () => { + await connect(); + + plugin.pushResponse(GenericV3Response.fromJson({} as any)); + + await client.exitChannel(); + + expect(plugin.latestRequest).to.be.instanceOf(ExitChannelRequest); + }); + + it('stepIn defaults to client.primaryThread and can be overridden', async () => { + await connect(); + client.primaryThread = 9; + + plugin.pushResponse(GenericV3Response.fromJson({} as any)); + await client.stepIn(); + expect(plugin.getLatestRequest().data.threadIndex).to.eql(9); + expect(plugin.getLatestRequest().data.stepType).to.eql(StepType.Line); + + plugin.pushResponse(GenericV3Response.fromJson({} as any)); + await client.stepIn(5); + expect(plugin.getLatestRequest().data.threadIndex).to.eql(5); + expect(plugin.getLatestRequest().data.stepType).to.eql(StepType.Line); + }); + + it('stepOver defaults to client.primaryThread and can be overridden', async () => { + await connect(); + client.primaryThread = 9; + + plugin.pushResponse(GenericV3Response.fromJson({} as any)); + await client.stepOver(); + expect(plugin.getLatestRequest().data.threadIndex).to.eql(9); + expect(plugin.getLatestRequest().data.stepType).to.eql(StepType.Over); + + plugin.pushResponse(GenericV3Response.fromJson({} as any)); + await client.stepOver(5); + expect(plugin.getLatestRequest().data.threadIndex).to.eql(5); + expect(plugin.getLatestRequest().data.stepType).to.eql(StepType.Over); + }); + + it('stepOut defaults to client.primaryThread and can be overridden', async () => { + await connect(); + client.primaryThread = 9; + + plugin.pushResponse(GenericV3Response.fromJson({} as any)); + await client.stepOut(); + expect(plugin.getLatestRequest().data.threadIndex).to.eql(9); + expect(plugin.getLatestRequest().data.stepType).to.eql(StepType.Out); + + plugin.pushResponse(GenericV3Response.fromJson({} as any)); + await client.stepOut(5); + expect(plugin.getLatestRequest().data.threadIndex).to.eql(5); + expect(plugin.getLatestRequest().data.stepType).to.eql(StepType.Out); + }); + + it('stepOut defaults to client.primaryThread and can be overridden', async () => { + await connect(); + + plugin.pushResponse(GenericV3Response.fromJson({} as any)); + + //does not send command because we're not stopped + client.isStopped = false; + await client.stepOut(); + expect(plugin.latestRequest).not.to.be.instanceof(StepRequest); + }); + + it('handles step cannot-continue response', async () => { + await connect(); + + plugin.pushResponse(GenericV3Response.fromJson({ + errorCode: ErrorCode.CANT_CONTINUE, + requestId: 12 + })); + + let cannotContinuePromise = client.once('cannot-continue'); + + client.isStopped = true; + await client.stepOut(); + + //if the cannot-continue event resolved, this test passed + await cannotContinuePromise; + }); + + describe('threads()', () => { + function thread(extra?: Partial) { + return { + isPrimary: true, + stopReason: StopReason.Break, + stopReasonDetail: 'because', + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs', + codeSnippet: 'sub main()', + ...extra ?? {} + }; + } + + it('skips sending command when not stopped', async () => { + await connect(); + + client.isStopped = false; + await client.threads(); + expect(plugin.latestRequest).not.to.be.instanceof(ThreadsResponse); + }); + + it('returns response even when error code is not ok', async () => { + await connect(); + + plugin.pushResponse(GenericV3Response.fromJson({ + errorCode: ErrorCode.CANT_CONTINUE, + requestId: 12 + })); + + const response = await client.threads(); + expect(response.data.errorCode).to.eql(ErrorCode.CANT_CONTINUE); + }); + + it('ignores the `isPrimary` flag when threadHoppingWorkaround is enabled', async () => { + await connect(); + client.protocolVersion = '2.0.0'; + client.primaryThread = 0; + plugin.pushResponse(ThreadsResponse.fromJson({ + requestId: 1, + threads: [ + thread({ + isPrimary: false + }), + thread({ + isPrimary: true + }) + ] + })); + + await client.threads(); + expect(client?.primaryThread).to.eql(0); + }); + + it('honors the `isPrimary` flag when threadHoppingWorkaround is disabled', async () => { + await connect(); + client.protocolVersion = '3.1.0'; + client.primaryThread = 0; + plugin.pushResponse(ThreadsResponse.fromJson({ + requestId: 1, + threads: [ + thread({ + isPrimary: false + }), + thread({ + isPrimary: true + }) + ] + })); + + await client.threads(); + expect(client?.primaryThread).to.eql(1); + }); + }); + + describe('getStackTrace', () => { + it('skips request if not stopped', async () => { + await connect(); + client.isStopped = false; + + await client.getStackTrace(); + expect(plugin.latestRequest).not.to.be.instanceof(StackTraceResponse); + }); + }); + + describe('executeCommand', () => { + it('skips sending command if not stopped', async () => { + await connect(); + client.isStopped = false; + await client.executeCommand('code'); + expect(plugin.latestRequest).not.instanceof(ExecuteRequest); + }); + + it('sends command when client is stopped', async () => { + await connect(); + + //the response structure doesn't matter, this test is to verify the request was properly built + plugin.pushResponse(ExecuteV3Response.fromJson({} as any)); + + const response = await client.executeCommand('print 123', 1, 2); + expect(plugin.getLatestRequest().data).to.include({ + requestId: plugin.latestRequest.data.requestId, + stackFrameIndex: 1, + threadIndex: 2, + sourceCode: 'print 123' + }); + }); + }); + + describe('addBreakpoints', () => { + it('returns the proper response', async () => { + await connect(); + + const responseBreakpoins = [{ + errorCode: 0, + id: 1, + ignoreCount: 0 + }, + { + errorCode: 0, + id: 1, + ignoreCount: 0 + }]; + plugin.pushResponse( + AddBreakpointsResponse.fromJson({ + requestId: 10, + breakpoints: responseBreakpoins + }) + ); + + const response = await client.addBreakpoints([{ + filePath: 'pkg:/source/main.brs', + lineNumber: 10 + }, { + filePath: 'pkg:/source/lib.brs', + lineNumber: 15 + }]); + expect(response.data.breakpoints).to.eql(responseBreakpoins); + }); + + it('sends AddBreakpointsRequest when conditional breakpoints are NOT supported', async () => { + await connect(); + client.protocolVersion = '2.0.0'; + + //response structure doesn't matter, we're verifying that the request was properly built + plugin.pushResponse(AddBreakpointsResponse.fromJson({} as any)); + await client.addBreakpoints([{ + filePath: 'pkg:/source/main.brs', + lineNumber: 12, + conditionalExpression: 'true or true' + }]); + + expect(plugin.getLatestRequest()).instanceof(AddBreakpointsRequest); + expect(plugin.getLatestRequest().data.breakpoints[0]).not.haveOwnProperty('conditionalExpression'); + }); + + it('sends AddConditionalBreakpointsRequest when conditional breakpoints ARE supported', async () => { + await connect(); + client.protocolVersion = '3.1.0'; + + //response structure doesn't matter, we're verifying that the request was properly built + plugin.pushResponse(AddConditionalBreakpointsResponse.fromJson({} as any)); + await client.addBreakpoints([{ + filePath: 'pkg:/source/main.brs', + lineNumber: 12, + conditionalExpression: 'true or true' + }]); + + expect(plugin.getLatestRequest()).instanceof(AddConditionalBreakpointsRequest); + expect(plugin.getLatestRequest().data.breakpoints[0].conditionalExpression).to.eql('true or true'); + }); + + it('includes complib prefix when supported', async () => { + await connect(); + client.protocolVersion = '3.1.0'; + + //response structure doesn't matter, we're verifying that the request was properly built + plugin.pushResponse(AddConditionalBreakpointsResponse.fromJson({} as any)); + await client.addBreakpoints([{ + filePath: 'pkg:/source/main.brs', + lineNumber: 12, + componentLibraryName: 'myapp' + }]); + + expect(plugin.getLatestRequest().data.breakpoints[0].filePath).to.eql('lib:/myapp/source/main.brs'); + }); + + it('excludes complib prefix when not supported', async () => { + await connect(); + client.protocolVersion = '2.0.0'; + + //response structure doesn't matter, we're verifying that the request was properly built + plugin.pushResponse(AddConditionalBreakpointsResponse.fromJson({} as any)); + await client.addBreakpoints([{ + filePath: 'pkg:/source/main.brs', + lineNumber: 12, + componentLibraryName: 'myapp' + }]); + + expect(plugin.getLatestRequest().data.breakpoints[0].filePath).to.eql('pkg:/source/main.brs'); + }); + }); + + describe('listBreakpoints', () => { + it('sends request when stopped', async () => { + await connect(); + client.isStopped = true; + + plugin.pushResponse(ListBreakpointsResponse.fromBuffer(null)); + await client.listBreakpoints(); + expect(plugin.latestRequest).instanceof(ListBreakpointsRequest); + }); + + it('sends request when running', async () => { + await connect(); + client.isStopped = false; + + plugin.pushResponse(ListBreakpointsResponse.fromBuffer(null)); + await client.listBreakpoints(); + expect(plugin.latestRequest).instanceof(ListBreakpointsRequest); + }); + }); + + describe('removeBreakpoints', () => { + it('sends breakpoint ids', async () => { + await connect(); + + //response structure doesn't matter, we're verifying that the request was properly built + plugin.pushResponse(RemoveBreakpointsResponse.fromJson({} as any)); + await client.removeBreakpoints([1, 2, 3]); + + expect(plugin.getLatestRequest().data.breakpointIds).to.eql([1, 2, 3]); + }); + + it('skips sending command if no breakpoints were provided', async () => { + await connect(); + + await client.removeBreakpoints(undefined); + expect(plugin.latestRequest).not.instanceof(RemoveBreakpointsRequest); + }); + }); + + it('knows when to enable complib specific breakpoints', () => { + //only supported on version 3.1.0 and above + client.protocolVersion = '1.0.0'; + expect( + client['enableComponentLibrarySpecificBreakpoints'] + ).to.be.false; + + client.protocolVersion = '3.0.0'; + expect( + client['enableComponentLibrarySpecificBreakpoints'] + ).to.be.false; + + client.protocolVersion = '3.1.0'; + expect( + client['enableComponentLibrarySpecificBreakpoints'] + ).to.be.true; + + client.protocolVersion = '4.0.0'; + expect( + client['enableComponentLibrarySpecificBreakpoints'] + ).to.be.true; + }); + + it('knows when to enable conditional breakpoints', () => { + //only supported on version 3.1.0 and above + client.protocolVersion = '1.0.0'; + expect( + client['supportsConditionalBreakpoints'] + ).to.be.false; + + client.protocolVersion = '3.0.0'; + expect( + client['supportsConditionalBreakpoints'] + ).to.be.false; + + client.protocolVersion = '3.1.0'; + expect( + client['supportsConditionalBreakpoints'] + ).to.be.true; + + client.protocolVersion = '4.0.0'; + expect( + client['supportsConditionalBreakpoints'] + ).to.be.true; + }); + + it('handles v3 handshake', async () => { + //these are false by default + expect(client.watchPacketLength).to.be.equal(false); + expect(client.isHandshakeComplete).to.be.equal(false); + + await client.connect(); + expect(plugin.responses[0]?.data).to.eql({ + packetLength: undefined, + requestId: HandshakeRequest.REQUEST_ID, + errorCode: ErrorCode.OK, + + magic: 'bsdebug', + protocolVersion: '3.1.0', + revisionTimestamp: new Date(2022, 1, 1) + } as HandshakeV3Response['data']); + + //version 3.0 includes packet length, so these should be true now + expect(client.watchPacketLength).to.be.equal(true); + expect(client.isHandshakeComplete).to.be.equal(true); + }); + + it('handles legacy handshake', async () => { + + expect(client.watchPacketLength).to.be.equal(false); + expect(client.isHandshakeComplete).to.be.equal(false); + + plugin.pushResponse( + HandshakeResponse.fromJson({ + magic: DebugProtocolClient.DEBUGGER_MAGIC, + protocolVersion: '1.0.0' + }) + ); + + await client.connect(); + + expect(client.watchPacketLength).to.be.equal(false); + expect(client.isHandshakeComplete).to.be.equal(true); + }); + + it('discards unrecognized updates', async () => { + await connect(); + + //known update type + plugin.server['client'].write( + ThreadAttachedUpdate.fromJson({ + stopReason: StopReason.Break, + stopReasonDetail: 'before', + threadIndex: 0 + }).toBuffer() + ); + //unknown update type + + //known update type + plugin.server['client'].write( + ThreadAttachedUpdate.fromJson({ + stopReason: StopReason.Break, + stopReasonDetail: 'after', + threadIndex: 1 + }).toBuffer() + ); + //unk + + // //we should have the two known update types + // expect(plugin.getRequest(-2)).to.eql(); + // expect(plugin.getRequest(-1)).to.eql(); + }); + + it('handles AllThreadsStoppedUpdate after handshake', async () => { + await client.connect(); + + const [, event] = await Promise.all([ + //wait for the client to suspend + client.once('suspend'), + //send an update which should cause the client to suspend + server.sendUpdate( + AllThreadsStoppedUpdate.fromJson({ + threadIndex: 1, + stopReason: StopReason.Break, + stopReasonDetail: 'test' + }) + ) + ]); + expect(event.data).include({ + threadIndex: 1, + stopReason: StopReason.Break, + stopReasonDetail: 'test' + }); + }); + + describe('getVariables', () => { + + it('skips sending the request if not stopped', async () => { + await connect(); + + client.isStopped = false; + + await client.getVariables(); + expect(plugin.latestRequest).not.instanceof(VariablesRequest); + }); + + it('returns `uninitialized` for never-defined leftmost variable', async () => { + await connect(); + + plugin.pushResponse( + GenericV3Response.fromJson({ + errorCode: ErrorCode.INVALID_ARGS, + requestId: 1, + errorData: { + missingKeyIndex: 0 + } + }) + ); + + //variable was never defined + const response = await client.getVariables(['notThere']); + expect(response.data.variables[0]).to.eql({ + name: 'notThere', + type: VariableType.Uninitialized, + value: null, + childCount: 0, + isConst: false, + isContainer: false, + refCount: 0 + } as Variable); + }); + + it('returns generic response when accessing a property on never-defined variable', async () => { + await connect(); + + plugin.pushResponse( + GenericV3Response.fromJson({ + errorCode: ErrorCode.INVALID_ARGS, + requestId: 1, + errorData: { + missingKeyIndex: 0 + } + }) + ); + + //another response for the "go one level up to get type info" request + plugin.pushResponse( + VariablesResponse.fromJson({ + requestId: 1, + variables: [{ + name: 'someObj', + type: VariableType.Uninitialized, + isConst: false, + isContainer: true, + refCount: 1, + value: undefined, + childCount: 2, + keyType: VariableType.String + }] + }) + ); + + //getting prop from variable that was never defined + await expectThrowsAsync(async () => { + await client.getVariables(['notThere', 'definitelyNotThere']); + }, `Cannot read 'definitelyNotThere' on type 'Uninitialized'`); + }); + + it('returns `invalid` when accessing a property on a defined AA', async () => { + await connect(); + + plugin.pushResponse( + GenericV3Response.fromJson({ + errorCode: ErrorCode.INVALID_ARGS, + requestId: 1, + errorData: { + missingKeyIndex: 1 + } + }) + ); + + //another response for the "go one level up to get type info" request + plugin.pushResponse( + VariablesResponse.fromJson({ + requestId: 1, + variables: [{ + name: 'there', + type: VariableType.AssociativeArray, + isConst: false, + isContainer: true, + refCount: 1, + value: undefined, + childCount: 2, + keyType: VariableType.String + }] + }) + ); + + //getting prop from variable that was never defined + const response = await client.getVariables(['there', 'notThere']); + expect(response.data.variables[0]).to.eql({ + name: 'notThere', + type: VariableType.Invalid, + value: 'Invalid (not defined)', + childCount: 0, + isConst: false, + isContainer: false, + refCount: 0 + } as Variable); + }); + + it('returns generic response when accessing a property on a property that does not exist', async () => { + await connect(); + + plugin.pushResponse( + GenericV3Response.fromJson({ + errorCode: ErrorCode.INVALID_ARGS, + requestId: 1, + errorData: { + missingKeyIndex: 1 + } + }) + ); + + //another response for the "go one level up to get type info" request + plugin.pushResponse( + VariablesResponse.fromJson({ + requestId: 1, + variables: [{ + name: 'notThere', + type: VariableType.Invalid, + isConst: false, + isContainer: false, + refCount: 1, + value: undefined + }] + }) + ); + + //getting prop from variable that was assigned to invalid (i.e. `setToInvalid = invalid`) + await expectThrowsAsync(async () => { + await client.getVariables(['there', 'notThere', 'definitelyNotThere']); + }, `Cannot read 'notThere' on type 'Invalid'`); + + //make sure we requested the correct variable + expect(plugin.getRequest(-1).data.variablePathEntries.map(x => x.name)).to.eql(['there']); + }); + + it('returns generic response when accessing a property on a property that does not exist in the middle', async () => { + await connect(); + + plugin.pushResponse( + GenericV3Response.fromJson({ + errorCode: ErrorCode.INVALID_ARGS, + requestId: 1, + errorData: { + missingKeyIndex: 1 + } + }) + ); + + //another response for the "go one level up to get type info" request + plugin.pushResponse( + VariablesResponse.fromJson({ + requestId: 1, + variables: [{ + name: 'notThere', + type: VariableType.Invalid, + isConst: false, + isContainer: false, + refCount: 1, + value: undefined + }] + }) + ); + //getting prop from variable that was assigned to invalid (i.e. `setToInvalid = invalid`) + await expectThrowsAsync(async () => { + await client.getVariables(['there', 'notThere', 'definitelyNotThere', 'reallyNotThere']); + }, `Cannot read 'notThere' on type 'Invalid'`); + + //make sure we requested the correct variable + expect(plugin.getRequest(-1).data.variablePathEntries.map(x => x.name)).to.eql(['there']); + }); + + it('shows node and subtype for failed prop access', async () => { + await connect(); + + plugin.pushResponse( + GenericV3Response.fromJson({ + errorCode: ErrorCode.INVALID_ARGS, + requestId: 1, + errorData: { + missingKeyIndex: 1 + } + }) + ); + + //another response for the "go one level up to get type info" request + plugin.pushResponse( + VariablesResponse.fromJson({ + requestId: 1, + variables: [{ + name: 'notThere', + type: VariableType.SubtypedObject, + isConst: false, + isContainer: false, + refCount: 1, + value: 'roSGNode; Node' + }] + }) + ); + //getting prop from variable that was assigned to invalid (i.e. `setToInvalid = invalid`) + await expectThrowsAsync(async () => { + await client.getVariables(['there', 'notThere', 'definitelyNotThere', 'reallyNotThere']); + }, `Cannot read 'notThere' on type 'roSGNode (Node)'`); + + //make sure we requested the correct variable + expect(plugin.getRequest(-1).data.variablePathEntries.map(x => x.name)).to.eql(['there']); + }); + + it('returns faked variable response with invalid', async () => { + await connect(); + + plugin.pushResponse( + GenericV3Response.fromJson({ + errorCode: ErrorCode.INVALID_ARGS, + requestId: 1, + errorData: { + invalidPathIndex: 0 + } + }) + ); + + //getting prop from variable that was assigned to invalid (i.e. `setToInvalid = invalid`) + const response = await client.getVariables(['notThere']); + expect(response?.data?.variables?.[0]?.type).to.eql(VariableType.Invalid); + }); + + it('throws when reading prop on invalid', async () => { + await connect(); + + plugin.pushResponse( + GenericV3Response.fromJson({ + errorCode: ErrorCode.INVALID_ARGS, + requestId: 1, + errorData: { + invalidPathIndex: 1 + } + }) + ); + + //another response for the "go one level up to get type info" request + plugin.pushResponse( + VariablesResponse.fromJson({ + requestId: 1, + variables: [{ + name: 'there', + type: VariableType.Invalid, + isConst: false, + isContainer: false, + refCount: 1, + value: 'Invalid' + }] + }) + ); + //getting prop from variable that was assigned to invalid (i.e. `setToInvalid = invalid`) + await expectThrowsAsync(async () => { + await client.getVariables(['there', 'setToInvalid', 'notThere']); + }, `Cannot read 'notThere' on type 'Invalid'`); + + //make sure we requested the correct variable + expect(plugin.getRequest(-1).data.variablePathEntries.map(x => x.name)).to.eql(['there', 'setToInvalid']); + }); + + it('returns invalid when left-hand item is an AA but right-hand item is missing', async () => { + await connect(); + + plugin.pushResponse( + GenericV3Response.fromJson({ + errorCode: ErrorCode.INVALID_ARGS, + requestId: 1, + errorData: { + invalidPathIndex: 1 + } + }) + ); + + //another response for the "go one level up to get type info" request + plugin.pushResponse( + VariablesResponse.fromJson({ + requestId: 1, + variables: [{ + name: 'there', + type: VariableType.Invalid, + isConst: false, + isContainer: false, + refCount: 1, + value: 'Invalid' + }] + }) + ); + //getting prop from variable that was assigned to invalid (i.e. `setToInvalid = invalid`) + await expectThrowsAsync(async () => { + await client.getVariables(['there', 'setToInvalid', 'notThere']); + }, `Cannot read 'notThere' on type 'Invalid'`); + + //make sure we requested the correct variable + expect(plugin.getRequest(-1).data.variablePathEntries.map(x => x.name)).to.eql(['there', 'setToInvalid']); + }); + + it('returns generic response when accessing a property on a variable with the value of `invalid`', async () => { + await connect(); + + plugin.pushResponse( + GenericV3Response.fromJson({ + errorCode: ErrorCode.INVALID_ARGS, + requestId: 1, + errorData: { + invalidPathIndex: 0 + } + }) + ); + + await expectThrowsAsync(async () => { + await client.getVariables(['setToInvalid', 'notThere']); + }, `Cannot read 'notThere'`); + }); + + it('returns generic response when accessing a property on a property with the value of `invalid`', async () => { + await connect(); + + //the initial response + plugin.pushResponse( + GenericV3Response.fromJson({ + errorCode: ErrorCode.INVALID_ARGS, + requestId: 1, + errorData: { + invalidPathIndex: 1 + } + }) + ); + + //another response for the "go one level up to get type info" request + plugin.pushResponse( + VariablesResponse.fromJson({ + requestId: 1, + variables: [{ + name: 'somePropWithValueSetToInvalid', + type: VariableType.Invalid, + isConst: false, + isContainer: false, + refCount: 1, + value: undefined + }] + }) + ); + + //getting prop from variable that was never defined + await expectThrowsAsync(async () => { + await client.getVariables(['someObj', 'somePropWithValueSetToInvalid', 'notThere']); + }, `Cannot read 'notThere' on type 'Invalid'`); + }); + + it('honors protocol version when deciding to send forceCaseInsensitive variable information', async () => { + await client.connect(); + //send the AllThreadsStopped event, and also wait for the client to suspend + await Promise.all([ + server.sendUpdate(AllThreadsStoppedUpdate.fromJson({ + threadIndex: 2, + stopReason: StopReason.Break, + stopReasonDetail: 'because' + })), + await client.once('suspend') + ]); + + // force the protocolVersion to 2.0.0 for this test + client.protocolVersion = '2.0.0'; + + plugin.pushResponse(VariablesResponse.fromJson({ + requestId: -1, // overridden in the plugin + variables: [] + })); + + await client.getVariables(['m', '"top"'], 1, 2); + expect( + VariablesRequest.fromBuffer(plugin.latestRequest.toBuffer()).data + ).to.eql({ + packetLength: 31, + requestId: 1, + command: Command.Variables, + enableForceCaseInsensitivity: false, + getChildKeys: true, + stackFrameIndex: 1, + threadIndex: 2, + variablePathEntries: [{ + name: 'm', + forceCaseInsensitive: false + }, { + name: 'top', + forceCaseInsensitive: false + }] + } as VariablesRequest['data']); + + // force the protocolVersion to 3.1.0 for this test + client.protocolVersion = '3.1.0'; + + plugin.pushResponse(VariablesResponse.fromJson({ + requestId: -1, // overridden in the plugin + variables: [] + })); + + await client.getVariables(['m', '"top"'], 1, 2); + expect( + VariablesRequest.fromBuffer(plugin.latestRequest.toBuffer()).data + ).to.eql({ + packetLength: 33, + requestId: 2, + command: Command.Variables, + enableForceCaseInsensitivity: true, + getChildKeys: true, + stackFrameIndex: 1, + threadIndex: 2, + variablePathEntries: [{ + name: 'm', + forceCaseInsensitive: true + }, { + name: 'top', + forceCaseInsensitive: false + }] + } as VariablesRequest['data']); + }); + }); + + describe('sendRequest', () => { + it('throws when controller is missing', async () => { + await connect(); + + delete client['controlSocket']; + await expectThrowsAsync(async () => { + await client.listBreakpoints(); + }, 'Control socket was closed - Command: ListBreakpoints'); + }); + + it('resolves only for matching requestId', async () => { + await connect(); + + plugin.pushResponse(ListBreakpointsResponse.fromJson({ + requestId: 10, + breakpoints: [{ + id: 123, + errorCode: 0, + ignoreCount: 2 + }] + })); + plugin.pushResponse(StackTraceV3Response.fromJson({ + requestId: 12, + entries: [] + })); + + //run both requests in quick succession so they both are listening to both responses + const [listBreakpointsResponse, getStackTraceResponse] = await Promise.all([ + client.listBreakpoints(), + client.getStackTrace() + ]); + expect(listBreakpointsResponse?.data.breakpoints[0]).to.include({ + id: 123, + errorCode: 0, + ignoreCount: 2 + }); + expect(getStackTraceResponse.data.entries).to.eql([]); + }); + + it('recovers on incomplete buffer', async () => { + await connect(); + + const buffer = AllThreadsStoppedUpdate.fromJson({ + stopReason: StopReason.Break, + stopReasonDetail: 'because', + threadIndex: 0 + }).toBuffer(); + + const dataReceivedPromise = client.once('data'); + const promise = client.once('suspend'); + + //write half the buffer + plugin.server['client'].write(buffer.slice(0, 5)); + //wait until we receive that data + await dataReceivedPromise; + //write the rest of the buffer + plugin.server['client'].write(buffer.slice(5)); + + //wait until the update shows up + const update = await promise; + expect(update.data.stopReasonDetail).to.eql('because'); + }); + }); + + describe('connectToIoPort', () => { + let ioServer: Net.Server; + let port: number; + let ioClient: Net.Socket; + let socketPromise: Promise; + + beforeEach(async () => { + port = await util.getPort(); + ioServer = new Net.Server(); + const deferred = defer(); + socketPromise = deferred.promise; + ioServer.listen({ + port: port, + hostName: '0.0.0.0' + }, () => { }); + ioServer.on('connection', (socket) => { + ioClient = socket; + ioClient.on('error', e => console.error(e)); + deferred.resolve(ioClient); + }); + ioServer.on('error', e => console.error(e)); + }); + + afterEach(() => { + try { + ioServer?.close(); + } catch { } + try { + ioClient?.destroy(); + } catch { } + }); + + it('supports the IOPortOpened update', async () => { + await connect(); + + const ioOutputPromise = client.once('io-output'); + + await plugin.server.sendUpdate(IOPortOpenedUpdate.fromJson({ + port: port + })); + + const socket = await socketPromise; + socket.write('hello\nworld\n'); + + const output = await ioOutputPromise; + expect(output).to.eql('hello\nworld'); + }); + + it('handles partial lines', async () => { + await connect(); + + await plugin.server.sendUpdate(IOPortOpenedUpdate.fromJson({ + port: port + })); + + const socket = await socketPromise; + const outputMonitors = [ + defer(), + defer(), + defer() + ]; + const output = []; + + const outputPromise = client.once('io-output'); + + client['ioSocket'].on('data', (data) => { + outputMonitors[output.length].resolve(); + output.push(data.toString()); + }); + + socket.write('hello '); + await outputMonitors[0].promise; + socket.write('world\n'); + await outputMonitors[1].promise; + expect(await outputPromise).to.eql('hello world'); + }); + + it('handles failed update', async () => { + await connect(); + const update = IOPortOpenedUpdate.fromJson({ + port: port + }); + update.success = false; + expect( + client['connectToIoPort'](update) + ).to.be.false; + }); + + it('terminates the ioClient on "end"', async () => { + await connect(); + await plugin.server.sendUpdate(IOPortOpenedUpdate.fromJson({ + port: port + })); + await socketPromise; + ioServer.close(); + }); + }); + + it('handles ThreadAttachedUpdate type', async () => { + await connect(); + + const promise = client.once('suspend'); + client.primaryThread = 1; + await plugin.server.sendUpdate(ThreadAttachedUpdate.fromJson({ + stopReason: StopReason.Break, + stopReasonDetail: 'because', + threadIndex: 2 + })); + await promise; + expect(client.primaryThread).to.eql(2); + }); +}); diff --git a/src/debugProtocol/client/DebugProtocolClient.ts b/src/debugProtocol/client/DebugProtocolClient.ts new file mode 100644 index 00000000..677139a4 --- /dev/null +++ b/src/debugProtocol/client/DebugProtocolClient.ts @@ -0,0 +1,1342 @@ +import * as Net from 'net'; +import * as debounce from 'debounce'; +import * as EventEmitter from 'eventemitter3'; +import * as semver from 'semver'; +import { PROTOCOL_ERROR_CODES, Command, StepType, ErrorCode, UpdateType, UpdateTypeCode, StopReason } from '../Constants'; +import { logger } from '../../logging'; +import { ExecuteV3Response } from '../events/responses/ExecuteV3Response'; +import { ListBreakpointsResponse } from '../events/responses/ListBreakpointsResponse'; +import { AddBreakpointsResponse } from '../events/responses/AddBreakpointsResponse'; +import { RemoveBreakpointsResponse } from '../events/responses/RemoveBreakpointsResponse'; +import { defer, util } from '../../util'; +import { BreakpointErrorUpdate } from '../events/updates/BreakpointErrorUpdate'; +import { ContinueRequest } from '../events/requests/ContinueRequest'; +import { StopRequest } from '../events/requests/StopRequest'; +import { ExitChannelRequest } from '../events/requests/ExitChannelRequest'; +import { StepRequest } from '../events/requests/StepRequest'; +import { RemoveBreakpointsRequest } from '../events/requests/RemoveBreakpointsRequest'; +import { ListBreakpointsRequest } from '../events/requests/ListBreakpointsRequest'; +import { VariablesRequest } from '../events/requests/VariablesRequest'; +import { StackTraceRequest } from '../events/requests/StackTraceRequest'; +import { ThreadsRequest } from '../events/requests/ThreadsRequest'; +import { ExecuteRequest } from '../events/requests/ExecuteRequest'; +import { AddBreakpointsRequest } from '../events/requests/AddBreakpointsRequest'; +import { AddConditionalBreakpointsRequest } from '../events/requests/AddConditionalBreakpointsRequest'; +import type { ProtocolRequest, ProtocolResponse, ProtocolUpdate } from '../events/ProtocolEvent'; +import { HandshakeResponse } from '../events/responses/HandshakeResponse'; +import { HandshakeV3Response } from '../events/responses/HandshakeV3Response'; +import { HandshakeRequest } from '../events/requests/HandshakeRequest'; +import { GenericV3Response } from '../events/responses/GenericV3Response'; +import { AllThreadsStoppedUpdate } from '../events/updates/AllThreadsStoppedUpdate'; +import { CompileErrorUpdate } from '../events/updates/CompileErrorUpdate'; +import { GenericResponse } from '../events/responses/GenericResponse'; +import type { StackTraceResponse } from '../events/responses/StackTraceResponse'; +import { ThreadsResponse } from '../events/responses/ThreadsResponse'; +import type { Variable } from '../events/responses/VariablesResponse'; +import { VariablesResponse, VariableType } from '../events/responses/VariablesResponse'; +import { IOPortOpenedUpdate, isIOPortOpenedUpdate } from '../events/updates/IOPortOpenedUpdate'; +import { ThreadAttachedUpdate } from '../events/updates/ThreadAttachedUpdate'; +import { StackTraceV3Response } from '../events/responses/StackTraceV3Response'; +import { ActionQueue } from '../../managers/ActionQueue'; +import type { DebugProtocolClientPlugin } from './DebugProtocolClientPlugin'; +import PluginInterface from '../PluginInterface'; +import type { VerifiedBreakpoint } from '../events/updates/BreakpointVerifiedUpdate'; +import { BreakpointVerifiedUpdate } from '../events/updates/BreakpointVerifiedUpdate'; +import type { AddConditionalBreakpointsResponse } from '../events/responses/AddConditionalBreakpointsResponse'; + +export class DebugProtocolClient { + + public logger = logger.createLogger(`[client]`); + + // The highest tested version of the protocol we support. + public supportedVersionRange = '<=3.0.0'; + + constructor( + options?: ConstructorOptions + ) { + this.options = { + controlPort: 8081, + host: undefined, + //override the defaults with the options from parameters + ...options ?? {} + }; + + //add the internal plugin last, so it's the final plugin to handle the events + this.addCorePlugin(); + } + + private addCorePlugin() { + this.plugins.add({ + onUpdate: (event) => { + return this.handleUpdate(event.update); + } + }, 999); + } + + public static DEBUGGER_MAGIC = 'bsdebug'; // 64-bit = [b'bsdebug\0' little-endian] + + public scriptTitle: string; + public isHandshakeComplete = false; + public connectedToIoPort = false; + /** + * Debug protocol version 3.0.0 introduced a packet_length to all responses. Prior to that, most responses had no packet length at all. + * This field indicates whether we should be looking for packet_length or not in the responses we get from the device + */ + public watchPacketLength = false; + public protocolVersion: string; + public primaryThread: number; + public stackFrameIndex: number; + + /** + * A collection of plugins that can interact with the client at lifecycle points + */ + public plugins = new PluginInterface(); + + private emitter = new EventEmitter(); + /** + * The primary socket for this session. It's used to communicate with the debugger by sending commands and receives responses or updates + */ + private controlSocket: Net.Socket; + /** + * Promise that is resolved when the control socket is closed + */ + private controlSocketClosed = defer(); + /** + * A socket where the debug server will send stdio + */ + private ioSocket: Net.Socket; + /** + * Resolves when the ioSocket has closed + */ + private ioSocketClosed = defer(); + /** + * The buffer where all unhandled data will be stored until successfully consumed + */ + private buffer = Buffer.alloc(0); + /** + * Is the debugger currently stopped at a line of code in the program + */ + public isStopped = false; + private requestIdSequence = 1; + private activeRequests = new Map(); + private options: ConstructorOptions; + + /** + * Prior to protocol v3.1.0, the Roku device would regularly set the wrong thread as "active", + * so this flag lets us know if we should use our better-than-nothing workaround + */ + private get enableThreadHoppingWorkaround() { + return semver.satisfies(this.protocolVersion, '<3.1.0'); + } + + /** + * Starting in protocol v3.1.0, component libary breakpoints must be added in the format `lib://`, but prior they didn't require this. + * So this flag tells us which format to support + */ + private get enableComponentLibrarySpecificBreakpoints() { + return semver.satisfies(this.protocolVersion, '>=3.1.0'); + } + + /** + * Starting in protocol v3.1.0, breakpoints can support conditional expressions. This flag indicates whether the current sessuion supports that functionality. + */ + private get supportsConditionalBreakpoints() { + return semver.satisfies(this.protocolVersion, '>=3.1.0'); + } + + public get supportsBreakpointRegistrationWhileRunning() { + return semver.satisfies(this.protocolVersion, '>=3.2.0'); + } + + public get supportsBreakpointVerification() { + return semver.satisfies(this.protocolVersion, '>=3.2.0'); + } + + /** + * Get a promise that resolves after an event occurs exactly once + */ + public once(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'start'): Promise; + public once(eventName: 'breakpoints-verified'): Promise; + public once(eventName: 'runtime-error' | 'suspend'): Promise; + public once(eventName: 'io-output'): Promise; + public once(eventName: 'data'): Promise; + public once(eventName: 'response'): Promise; + public once(eventName: 'update'): Promise; + public once(eventName: 'protocol-version'): Promise; + public once(eventName: 'handshake-verified'): Promise; + public once(eventName: string) { + return new Promise((resolve) => { + const disconnect = this.on(eventName as Parameters[0], (...args) => { + disconnect(); + resolve(...args); + }); + }); + } + + public on(eventName: 'compile-error', handler: (event: CompileErrorUpdate) => void); + public on(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'start', handler: () => void); + public on(eventName: 'breakpoints-verified', handler: (event: BreakpointsVerifiedEvent) => void); + public on(eventName: 'response', handler: (response: ProtocolResponse) => void); + public on(eventName: 'update', handler: (update: ProtocolUpdate) => void); + /** + * The raw data from the server socket. You probably don't need this... + */ + public on(eventName: 'data', handler: (data: Buffer) => void); + public on(eventName: 'runtime-error' | 'suspend', handler: (data: T) => void); + public on(eventName: 'io-output', handler: (output: string) => void); + public on(eventName: 'protocol-version', handler: (data: ProtocolVersionDetails) => void); + public on(eventName: 'handshake-verified', handler: (data: HandshakeResponse) => void); + // public on(eventname: 'rendezvous', handler: (output: RendezvousHistory) => void); + // public on(eventName: 'runtime-error', handler: (error: BrightScriptRuntimeError) => void); + public on(eventName: string, handler: (payload: any) => void) { + this.emitter.on(eventName, handler); + return () => { + this.emitter.removeListener(eventName, handler); + }; + } + + private emit(eventName: 'compile-error', response: CompileErrorUpdate); + private emit(eventName: 'response', response: ProtocolResponse); + private emit(eventName: 'update', update: ProtocolUpdate); + private emit(eventName: 'data', update: Buffer); + private emit(eventName: 'breakpoints-verified', event: BreakpointsVerifiedEvent); + private emit(eventName: 'suspend' | 'runtime-error', data: AllThreadsStoppedUpdate | ThreadAttachedUpdate); + private emit(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'handshake-verified' | 'io-output' | 'protocol-version' | 'start', data?); + private emit(eventName: string, data?) { + //emit these events on next tick, otherwise they will be processed immediately which could cause issues + setTimeout(() => { + //in rare cases, this event is fired after the debugger has closed, so make sure the event emitter still exists + this.emitter.emit(eventName, data); + }, 0); + } + + /** + * A function that can be used to cancel the repeating interval that's running to try and establish a connection to the control socket. + */ + private cancelControlConnectInterval: () => void; + + /** + * A collection of sockets created when trying to connect to the debug protocol's control socket. We keep these around for quicker tear-down + * whenever there is an early-terminated debug session + */ + private pendingControlConnectionSockets: Set; + + private async establishControlConnection() { + this.pendingControlConnectionSockets = new Set(); + const connection = await new Promise((resolve) => { + this.cancelControlConnectInterval = util.setInterval((cancelInterval) => { + const socket = new Net.Socket({ + allowHalfOpen: false + }); + this.pendingControlConnectionSockets.add(socket); + socket.on('error', (error) => { + console.debug(Date.now(), 'Encountered an error connecting to the debug protocol socket. Ignoring and will try again soon', error); + }); + socket.connect({ port: this.options.controlPort, host: this.options.host }, () => { + cancelInterval(); + + this.logger.debug(`Connected to debug protocol control port. Socket ${[...this.pendingControlConnectionSockets].indexOf(socket)} of ${this.pendingControlConnectionSockets.size} was the winner`); + //clean up all remaining pending sockets + for (const pendingSocket of this.pendingControlConnectionSockets) { + pendingSocket.removeAllListeners(); + //cleanup and destroy all other sockets + if (pendingSocket !== socket) { + pendingSocket.end(); + pendingSocket?.destroy(); + } + } + this.pendingControlConnectionSockets.clear(); + resolve(socket); + }); + }, this.options.controlConnectInterval ?? 250); + }); + await this.plugins.emit('onServerConnected', { + client: this, + server: connection + }); + return connection; + } + + /** + * A queue for processing the incoming buffer, every transmission at a time + */ + private bufferQueue = new ActionQueue(); + + /** + * Connect to the debug server. + * @param sendHandshake should the handshake be sent as part of this connect process. If false, `.sendHandshake()` will need to be called before a session can begin + */ + public async connect(sendHandshake = true): Promise { + this.logger.log('connect', this.options); + + // If there is no error, the server has accepted the request and created a new dedicated control socket + this.controlSocket = await this.establishControlConnection(); + + this.controlSocket.on('data', (data) => { + this.writeToBufferLog('server-to-client', data); + this.emit('data', data); + //queue up processing the new data, chunk by chunk + void this.bufferQueue.run(async () => { + this.buffer = Buffer.concat([this.buffer, data]); + while (this.buffer.length > 0 && await this.process()) { + //the loop condition is the actual work + } + return true; + }); + }); + + this.controlSocket.on('close', () => { + this.logger.log('Control socket closed'); + this.controlSocketClosed.tryResolve(); + void this.shutdown('app-exit'); + }); + + // Don't forget to catch error, for your own sake. + this.controlSocket.once('error', (error) => { + //the Roku closed the connection for some unknown reason... + this.logger.error(`error on control port`, error); + this.controlSocket?.destroy?.(); + void this.shutdown('close'); + }); + + if (sendHandshake) { + await this.sendHandshake(); + } + return true; + } + + /** + * Send the initial handshake request, and wait for the handshake response + */ + public async sendHandshake() { + return this.processHandshakeRequest( + HandshakeRequest.fromJson({ + magic: DebugProtocolClient.DEBUGGER_MAGIC + }) + ); + } + + private async processHandshakeRequest(request: HandshakeRequest) { + //send the magic, which triggers the debug session + this.logger.log('Sending magic to server'); + + //send the handshake request, and wait for the handshake response from the device + return this.sendRequest(request); + } + + /** + * Write a specific buffer log entry to the logger, which, when file logging is enabled + * can be extracted and processed through the DebugProtocolClientReplaySession + */ + private writeToBufferLog(type: 'server-to-client' | 'client-to-server' | 'io', buffer: Buffer) { + let obj = { + type: type, + timestamp: new Date().toISOString(), + buffer: buffer.toJSON() + }; + if (type === 'io') { + (obj as any).text = buffer.toString(); + } + this.logger.log('[[bufferLog]]:', JSON.stringify(obj)); + } + + public continue() { + return this.processContinueRequest( + ContinueRequest.fromJson({ + requestId: this.requestIdSequence++ + }) + ); + } + + private async processContinueRequest(request: ContinueRequest) { + if (this.isStopped) { + this.isStopped = false; + return this.sendRequest(request); + } + } + + public pause() { + return this.processStopRequest( + StopRequest.fromJson({ + requestId: this.requestIdSequence++ + }) + ); + } + + private async processStopRequest(request: StopRequest) { + if (this.isStopped === false) { + return this.sendRequest(request); + } + } + + /** + * Send the "exit channel" command, which will tell the debug session to immediately quit + */ + public async exitChannel() { + return this.sendRequest( + ExitChannelRequest.fromJson({ + requestId: this.requestIdSequence++ + }) + ); + } + + public async stepIn(threadIndex: number = this.primaryThread) { + return this.step(StepType.Line, threadIndex); + } + + public async stepOver(threadIndex: number = this.primaryThread) { + return this.step(StepType.Over, threadIndex); + } + + public async stepOut(threadIndex: number = this.primaryThread) { + return this.step(StepType.Out, threadIndex); + } + + private async step(stepType: StepType, threadIndex: number): Promise { + return this.processStepRequest( + StepRequest.fromJson({ + requestId: this.requestIdSequence++, + stepType: stepType, + threadIndex: threadIndex + }) + ); + } + + private async processStepRequest(request: StepRequest) { + if (this.isStopped) { + this.isStopped = false; + let stepResult = await this.sendRequest(request); + if (stepResult.data.errorCode === ErrorCode.OK) { + this.isStopped = true; + //TODO this is not correct. Do we get a new threads event after a step? Perhaps that should be what triggers the event instead of us? + this.emit('suspend', stepResult as AllThreadsStoppedUpdate); + } else { + // there is a CANT_CONTINUE error code but we can likely treat all errors like a CANT_CONTINUE + this.emit('cannot-continue'); + } + return stepResult; + } else { + this.logger.log('[processStepRequest] skipped because debugger is not paused'); + } + } + + public async threads() { + return this.processThreadsRequest( + ThreadsRequest.fromJson({ + requestId: this.requestIdSequence++ + }) + ); + } + public async processThreadsRequest(request: ThreadsRequest) { + if (this.isStopped) { + let result = await this.sendRequest(request); + + if (result.data.errorCode === ErrorCode.OK) { + //older versions of the debug protocol had issues with maintaining the active thread, so our workaround is to keep track of it elsewhere + if (this.enableThreadHoppingWorkaround) { + //ignore the `isPrimary` flag on threads + this.logger.debug(`Ignoring the 'isPrimary' flag from threads because protocol version 3.0.0 and lower has a bug`); + } else { + //trust the debug protocol's `isPrimary` flag on threads + for (let i = 0; i < result.data.threads.length; i++) { + let thread = result.data.threads[i]; + if (thread.isPrimary) { + this.primaryThread = i; + break; + } + } + } + } + return result; + } else { + this.logger.log('[processThreadsRequest] skipped because not stopped'); + } + } + + /** + * Get the stackTrace from the device IF currently stopped + */ + public async getStackTrace(threadIndex: number = this.primaryThread) { + return this.processStackTraceRequest( + StackTraceRequest.fromJson({ + requestId: this.requestIdSequence++, + threadIndex: threadIndex + }) + ); + } + + private async processStackTraceRequest(request: StackTraceRequest) { + if (!this.isStopped) { + this.logger.log('[getStackTrace] skipped because debugger is not paused'); + } else if (request?.data?.threadIndex > -1) { + return this.sendRequest(request); + } else { + this.logger.log(`[getStackTrace] skipped because ${request?.data?.threadIndex} is not valid threadIndex`); + } + } + + /** + * @param variablePathEntries One or more path entries to the variable to be inspected. E.g., m.top.myObj["someKey"] can be accessed with ["m","top","myobj","\"someKey\""]. + * + * If no path is specified, the variables accessible from the specified stack frame are returned. + * + * Starting in protocol v3.1.0, The keys for indexed gets (i.e. obj["key"]) should be wrapped in quotes so they can be handled in a case-sensitive fashion (if applicable on device). + * All non-quoted keys (i.e. strings without leading and trailing quotes inside them) will be treated as case-insensitive). + * @param getChildKeys If set, VARIABLES response include the child keys for container types like lists and associative arrays + * @param stackFrameIndex 0 = first function called, nframes-1 = last function. This indexing does not match the order of the frames returned from the STACKTRACE command + * @param threadIndex the index (or perhaps ID?) of the thread to get variables for + */ + public async getVariables(variablePathEntries: Array = [], stackFrameIndex: number = this.stackFrameIndex, threadIndex: number = this.primaryThread) { + const response = await this.processVariablesRequest( + VariablesRequest.fromJson({ + requestId: this.requestIdSequence++, + threadIndex: threadIndex, + stackFrameIndex: stackFrameIndex, + getChildKeys: true, + variablePathEntries: variablePathEntries.map(x => ({ + //remove leading and trailing quotes + name: x.replace(/^"/, '').replace(/"$/, ''), + forceCaseInsensitive: !x.startsWith('"') && !x.endsWith('"') + })), + //starting in protocol v3.1.0, it supports marking certain path items as case-insensitive (i.e. parts of DottedGet expressions) + enableForceCaseInsensitivity: semver.satisfies(this.protocolVersion, '>=3.1.0') && variablePathEntries.length > 0 + }) + ); + + //if there was an issue, build a "fake" variables response for several known situationsm or throw nicer errors + if (util.hasNonNullishProperty(response?.data.errorData)) { + let variable = { + value: null, + isContainer: false, + isConst: false, + refCount: 0, + childCount: 0 + } as Variable; + const simulatedResponse = VariablesResponse.fromJson({ + ...response.data, + variables: [variable] + }); + + let parentVarType: VariableType; + let parentVarTypeText: string; + const loadParentVarInfo = async (index: number) => { + //fetch the variable one level back from the bad one to get its type + const parentVar = await this.getVariables( + variablePathEntries.slice(0, index), + stackFrameIndex, + threadIndex + ); + parentVarType = parentVar?.data?.variables?.[0]?.type; + parentVarTypeText = parentVarType; + //convert `roSGNode; Node` to `roSGNode (Node)` + if (parentVarType === VariableType.SubtypedObject) { + const chunks = parentVar?.data?.variables?.[0]?.value?.toString().split(';').map(x => x.trim()); + parentVarTypeText = `${chunks[0]} (${chunks[1]})`; + } + }; + + if (!util.isNullish(response.data.errorData.missingKeyIndex)) { + const { missingKeyIndex } = response.data.errorData; + //leftmost var is uninitialized, and we tried to read it + //ex: variablePathEntries = [`notThere`] + if (variablePathEntries.length === 1 && missingKeyIndex === 0) { + variable.name = variablePathEntries[0]; + variable.type = VariableType.Uninitialized; + return simulatedResponse; + } + + //leftmost var was uninitialized, and tried to read a prop on it + //ex: variablePathEntries = ["notThere", "definitelyNotThere"] + if (missingKeyIndex === 0 && variablePathEntries.length > 1) { + throw new Error(`Cannot read '${variablePathEntries[missingKeyIndex + 1]}' on type 'Uninitialized'`); + } + + if (variablePathEntries.length > 1 && missingKeyIndex > 0) { + await loadParentVarInfo(missingKeyIndex); + + // prop at the end of Node or AA doesn't exist. Treat like `invalid`. + // ex: variablePathEntries = ['there', 'notThere'] + if ( + missingKeyIndex === variablePathEntries.length - 1 && + [VariableType.AssociativeArray, VariableType.SubtypedObject].includes(parentVarType) + ) { + variable.name = variablePathEntries[variablePathEntries.length - 1]; + variable.type = VariableType.Invalid; + variable.value = 'Invalid (not defined)'; + return simulatedResponse; + } + } + //prop in the middle is missing, tried reading a prop on it + // ex: variablePathEntries = ["there", "notThere", "definitelyNotThere"] + throw new Error(`Cannot read '${variablePathEntries[missingKeyIndex]}'${parentVarType ? ` on type '${parentVarTypeText}'` : ''}`); + } + + //this flow is when the item at the index exists, but is set to literally `invalid` or is an unknown value + if (!util.isNullish(response.data.errorData.invalidPathIndex)) { + const { invalidPathIndex } = response.data.errorData; + + //leftmost var is literal `invalid`, tried to read it + if (variablePathEntries.length === 1 && invalidPathIndex === 0) { + variable.name = variablePathEntries[variablePathEntries.length - 1]; + variable.type = VariableType.Invalid; + return simulatedResponse; + } + + if ( + variablePathEntries.length > 1 && + invalidPathIndex > 0 && + //only do this logic if the invalid item is not the last item + invalidPathIndex < variablePathEntries.length - 1 + ) { + await loadParentVarInfo(invalidPathIndex + 1); + + //leftmost var is set to literal `invalid`, tried to read prop + if (invalidPathIndex === 0 && variablePathEntries.length > 1) { + throw new Error(`Cannot read '${variablePathEntries[invalidPathIndex + 1]}' on type '${parentVarTypeText}'`); + } + + // prop at the end doesn't exist. Treat like `invalid`. + // ex: variablePathEntries = ['there', 'notThere'] + if ( + invalidPathIndex === variablePathEntries.length - 1 && + [VariableType.AssociativeArray, VariableType.SubtypedObject].includes(parentVarType) + ) { + variable.name = variablePathEntries[variablePathEntries.length - 1]; + variable.type = VariableType.Invalid; + variable.value = 'Invalid (not defined)'; + return simulatedResponse; + } + } + console.log('Bronley'); + //prop in the middle is missing, tried reading a prop on it + // ex: variablePathEntries = ["there", "thereButSetToInvalid", "definitelyNotThere"] + throw new Error(`Cannot read '${variablePathEntries[invalidPathIndex + 1]}'${parentVarType ? ` on type '${parentVarTypeText}'` : ''}`); + } + } + return response; + } + + private async processVariablesRequest(request: VariablesRequest) { + if (this.isStopped && request.data.threadIndex > -1) { + return this.sendRequest(request); + } + } + + public async executeCommand(sourceCode: string, stackFrameIndex: number = this.stackFrameIndex, threadIndex: number = this.primaryThread) { + return this.processExecuteRequest( + ExecuteRequest.fromJson({ + requestId: this.requestIdSequence++, + threadIndex: threadIndex, + stackFrameIndex: stackFrameIndex, + sourceCode: sourceCode + }) + ); + } + + private async processExecuteRequest(request: ExecuteRequest) { + if (this.isStopped && request.data.threadIndex > -1) { + return this.sendRequest(request); + } + } + + public async addBreakpoints(breakpoints: Array): Promise { + const { enableComponentLibrarySpecificBreakpoints } = this; + if (breakpoints?.length > 0) { + const json = { + requestId: this.requestIdSequence++, + breakpoints: breakpoints.map(x => { + let breakpoint = { + ...x, + ignoreCount: x.hitCount + }; + if (enableComponentLibrarySpecificBreakpoints && breakpoint.componentLibraryName) { + breakpoint.filePath = breakpoint.filePath.replace(/^pkg:\//i, `lib:/${breakpoint.componentLibraryName}/`); + } + return breakpoint; + }) + }; + + const useConditionalBreakpoints = ( + //does this protocol version support conditional breakpoints? + this.supportsConditionalBreakpoints && + //is there at least one conditional breakpoint present? + !!breakpoints.find(x => !!x?.conditionalExpression?.trim()) + ); + + let response: AddBreakpointsResponse | AddConditionalBreakpointsResponse; + if (useConditionalBreakpoints) { + response = await this.sendRequest( + AddConditionalBreakpointsRequest.fromJson(json) + ); + } else { + response = await this.sendRequest( + AddBreakpointsRequest.fromJson(json) + ); + } + + //if the device does not support breakpoint verification, then auto-mark all of these as verified + if (!this.supportsBreakpointVerification) { + this.emit('breakpoints-verified', { + breakpoints: response.data.breakpoints + }); + } + return response; + } + return AddBreakpointsResponse.fromBuffer(null); + } + + public async listBreakpoints(): Promise { + return this.processRequest( + ListBreakpointsRequest.fromJson({ + requestId: this.requestIdSequence++ + }) + ); + } + + /** + * Remove breakpoints having the specified IDs + */ + public async removeBreakpoints(breakpointIds: number[]) { + return this.processRemoveBreakpointsRequest( + RemoveBreakpointsRequest.fromJson({ + requestId: this.requestIdSequence++, + breakpointIds: breakpointIds + }) + ); + } + + private async processRemoveBreakpointsRequest(request: RemoveBreakpointsRequest) { + //throw out null breakpoints + request.data.breakpointIds = request.data.breakpointIds?.filter(x => typeof x === 'number') ?? []; + + if (request.data.breakpointIds?.length > 0) { + return this.sendRequest(request); + } + return RemoveBreakpointsResponse.fromJson(null); + } + + /** + * Given a request, process it in the proper fashion. This is mostly used for external mocking/testing of + * this client, but it should force the client to flow in the same fashion as a live debug session + */ + public async processRequest(request: ProtocolRequest): Promise { + switch (request?.constructor.name) { + case ContinueRequest.name: + return this.processContinueRequest(request as ContinueRequest) as any; + + case ExecuteRequest.name: + return this.processExecuteRequest(request as ExecuteRequest) as any; + + case HandshakeRequest.name: + return this.processHandshakeRequest(request as HandshakeRequest) as any; + + case RemoveBreakpointsRequest.name: + return this.processRemoveBreakpointsRequest(request as RemoveBreakpointsRequest) as any; + + case StackTraceRequest.name: + return this.processStackTraceRequest(request as StackTraceRequest) as any; + + case StepRequest.name: + return this.processStepRequest(request as StepRequest) as any; + + case StopRequest.name: + return this.processStopRequest(request as StopRequest) as any; + + case ThreadsRequest.name: + return this.processThreadsRequest(request as ThreadsRequest) as any; + + case VariablesRequest.name: + return this.processVariablesRequest(request as VariablesRequest) as any; + + //for all other request types, there's no custom business logic, so just pipe them through manually + case AddBreakpointsRequest.name: + case AddConditionalBreakpointsRequest.name: + case ExitChannelRequest.name: + case ListBreakpointsRequest.name: + return this.sendRequest(request); + default: + this.logger.log('Unknown request type. Sending anyway...', request); + //unknown request type. try sending it as-is + return this.sendRequest(request); + } + } + + /** + * Send a request to the roku device, and get a promise that resolves once we have received the response + */ + private async sendRequest(request: ProtocolRequest) { + request = (await this.plugins.emit('beforeSendRequest', { + client: this, + request: request + })).request; + + this.activeRequests.set(request.data.requestId, request); + + return new Promise((resolve, reject) => { + let unsubscribe = this.on('response', (response) => { + if (response.data.requestId === request.data.requestId) { + unsubscribe(); + this.activeRequests.delete(request.data.requestId); + resolve(response as T); + } + }); + + this.logEvent(request); + if (this.controlSocket) { + const buffer = request.toBuffer(); + this.writeToBufferLog('client-to-server', buffer); + this.controlSocket.write(buffer); + void this.plugins.emit('afterSendRequest', { + client: this, + request: request + }); + } else { + reject( + new Error(`Control socket was closed - Command: ${Command[request.data.command]}`) + ); + } + }); + } + + /** + * Sometimes a request arrives that we don't understand. If that's the case, this function can be used + * to discard that entire response by discarding `packet_length` number of bytes + */ + private discardNextResponseOrUpdate() { + const response = GenericV3Response.fromBuffer(this.buffer); + if (response.success && response.data.packetLength > 0) { + this.logger.warn(`Unsupported response or updated encountered. Discarding ${response.data.packetLength} bytes:`, JSON.stringify( + this.buffer.slice(0, response.data.packetLength + 1).toJSON().data + )); + //we have a valid event. Clear the buffer of this data + this.buffer = this.buffer.slice(response.data.packetLength); + } + } + + /** + * A counter to help give a unique id to each update (mostly just for logging purposes) + */ + private updateSequence = 1; + + private logEvent(event: ProtocolRequest | ProtocolResponse | ProtocolUpdate) { + const [, eventName, eventType] = /(.+?)((?:v\d+_?\d*)?(?:request|response|update))/ig.exec(event?.constructor.name) ?? []; + if (isProtocolRequest(event)) { + this.logger.log(`${eventName} ${event.data.requestId} (${eventType})`, event, `(${event?.constructor.name})`); + } else if (isProtocolUpdate(event)) { + this.logger.log(`${eventName} ${this.updateSequence++} (${eventType})`, event, `(${event?.constructor.name})`); + } else { + if (event.data.errorCode === ErrorCode.OK) { + this.logger.log(`${eventName} ${event.data.requestId} (${eventType})`, event, `(${event?.constructor.name})`); + } else { + this.logger.log(`[error] ${eventName} ${event.data.requestId} (${eventType})`, event, `(${event?.constructor.name})`); + } + } + } + + private async process(): Promise { + try { + this.logger.info('[process()]: buffer=', this.buffer.toJSON()); + + let { responseOrUpdate } = await this.plugins.emit('provideResponseOrUpdate', { + client: this, + activeRequests: this.activeRequests, + buffer: this.buffer + }); + + if (!responseOrUpdate) { + responseOrUpdate = await this.getResponseOrUpdate(this.buffer); + } + + //if the event failed to parse, or the buffer doesn't have enough bytes to satisfy the packetLength, exit here (new data will re-trigger this function) + if (!responseOrUpdate) { + this.logger.info('Unable to convert buffer into anything meaningful', this.buffer); + //if we have packet length, and we have at least that many bytes, throw out this message so we can hopefully recover + this.discardNextResponseOrUpdate(); + return false; + } + if (!responseOrUpdate.success || responseOrUpdate.data.packetLength > this.buffer.length) { + this.logger.log(`event parse failed. ${responseOrUpdate?.data?.packetLength} bytes required, ${this.buffer.length} bytes available`); + return false; + } + + //we have a valid event. Remove this data from the buffer + this.buffer = this.buffer.slice(responseOrUpdate.readOffset); + + if (responseOrUpdate.data.errorCode !== ErrorCode.OK) { + this.logEvent(responseOrUpdate); + } + + //we got a result + if (responseOrUpdate) { + //emit the corresponding event + if (isProtocolUpdate(responseOrUpdate)) { + this.logEvent(responseOrUpdate); + this.emit('update', responseOrUpdate); + await this.plugins.emit('onUpdate', { + client: this, + update: responseOrUpdate + }); + } else { + this.logEvent(responseOrUpdate); + this.emit('response', responseOrUpdate); + await this.plugins.emit('onResponse', { + client: this, + response: responseOrUpdate as any + }); + } + return true; + } + } catch (e) { + this.logger.error(`process() failed:`, e); + } + } + + /** + * Given a buffer, try to parse into a specific ProtocolResponse or ProtocolUpdate + */ + public async getResponseOrUpdate(buffer: Buffer): Promise { + //if we haven't seen a handshake yet, try to convert the buffer into a handshake + if (!this.isHandshakeComplete) { + let handshake: HandshakeV3Response | HandshakeResponse; + //try building the v3 handshake response first + handshake = HandshakeV3Response.fromBuffer(buffer); + //we didn't get a v3 handshake. try building an older handshake response + if (!handshake.success) { + handshake = HandshakeResponse.fromBuffer(buffer); + } + if (handshake.success) { + await this.verifyHandshake(handshake); + return handshake; + } + return; + } + + let genericResponse = this.watchPacketLength ? GenericV3Response.fromBuffer(buffer) : GenericResponse.fromBuffer(buffer); + + //if the response has a non-OK error code, we won't receive the expected response type, + //so return the generic response + if (genericResponse.success && genericResponse.data.errorCode !== ErrorCode.OK) { + return genericResponse; + } + // a nonzero requestId means this is a response to a request that we sent + if (genericResponse.data.requestId !== 0) { + //requestId 0 means this is an update + const request = this.activeRequests.get(genericResponse.data.requestId); + if (request) { + return DebugProtocolClient.getResponse(this.buffer, request.data.command); + } + } else { + return this.getUpdate(this.buffer); + } + } + + public static getResponse(buffer: Buffer, command: Command) { + switch (command) { + case Command.Stop: + case Command.Continue: + case Command.Step: + case Command.ExitChannel: + return GenericV3Response.fromBuffer(buffer); + case Command.Execute: + return ExecuteV3Response.fromBuffer(buffer); + case Command.AddBreakpoints: + case Command.AddConditionalBreakpoints: + return AddBreakpointsResponse.fromBuffer(buffer); + case Command.ListBreakpoints: + return ListBreakpointsResponse.fromBuffer(buffer); + case Command.RemoveBreakpoints: + return RemoveBreakpointsResponse.fromBuffer(buffer); + case Command.Variables: + return VariablesResponse.fromBuffer(buffer); + case Command.StackTrace: + return StackTraceV3Response.fromBuffer(buffer); + case Command.Threads: + return ThreadsResponse.fromBuffer(buffer); + default: + return undefined; + } + } + + public getUpdate(buffer: Buffer): ProtocolUpdate { + //read the update_type from the buffer (save some buffer parsing time by narrowing to the exact update type) + const updateTypeCode = buffer.readUInt32LE( + // if the protocol supports packet length, then update_type is bytes 12-16. Otherwise, it's bytes 8-12 + this.watchPacketLength ? 12 : 8 + ); + const updateType = UpdateTypeCode[updateTypeCode] as UpdateType; + + this.logger?.log('getUpdate(): update Type:', updateType); + switch (updateType) { + case UpdateType.IOPortOpened: + //TODO handle this + return IOPortOpenedUpdate.fromBuffer(buffer); + case UpdateType.AllThreadsStopped: + return AllThreadsStoppedUpdate.fromBuffer(buffer); + case UpdateType.ThreadAttached: + return ThreadAttachedUpdate.fromBuffer(buffer); + case UpdateType.BreakpointError: + //we do nothing with breakpoint errors at this time. + return BreakpointErrorUpdate.fromBuffer(buffer); + case UpdateType.CompileError: + let compileErrorUpdate = CompileErrorUpdate.fromBuffer(buffer); + if (compileErrorUpdate?.data?.errorMessage !== '') { + this.emit('compile-error', compileErrorUpdate); + } + return compileErrorUpdate; + case UpdateType.BreakpointVerified: + let response = BreakpointVerifiedUpdate.fromBuffer(buffer); + if (response?.data?.breakpoints?.length > 0) { + this.emit('breakpoints-verified', response.data); + } + return response; + default: + return undefined; + } + } + + private handleUpdateQueue = new ActionQueue(); + + /** + * Handle/process any received updates from the debug protocol + */ + private async handleUpdate(update: ProtocolUpdate) { + return this.handleUpdateQueue.run(async () => { + update = (await this.plugins.emit('beforeHandleUpdate', { + client: this, + update: update + })).update; + + if (update instanceof AllThreadsStoppedUpdate || update instanceof ThreadAttachedUpdate) { + this.isStopped = true; + + let eventName: 'runtime-error' | 'suspend'; + if (update.data.stopReason === StopReason.RuntimeError) { + eventName = 'runtime-error'; + } else { + eventName = 'suspend'; + } + + const isValidStopReason = [StopReason.RuntimeError, StopReason.Break, StopReason.StopStatement].includes(update.data.stopReason); + + if (update instanceof AllThreadsStoppedUpdate && isValidStopReason) { + this.primaryThread = update.data.threadIndex; + this.stackFrameIndex = 0; + this.emit(eventName, update); + } else if (update instanceof ThreadAttachedUpdate && isValidStopReason) { + this.primaryThread = update.data.threadIndex; + this.emit(eventName, update); + } + + } else if (isIOPortOpenedUpdate(update)) { + this.connectToIoPort(update); + } + return true; + }); + } + + /** + * Verify all the handshake data + */ + private async verifyHandshake(response: HandshakeResponse | HandshakeV3Response): Promise { + if (DebugProtocolClient.DEBUGGER_MAGIC === response.data.magic) { + this.logger.log('Magic is valid.'); + + this.protocolVersion = response.data.protocolVersion; + this.logger.log('Protocol Version:', this.protocolVersion); + + this.watchPacketLength = semver.satisfies(this.protocolVersion, '>=3.0.0'); + this.isHandshakeComplete = true; + + let handshakeVerified = true; + + if (semver.satisfies(this.protocolVersion, this.supportedVersionRange)) { + this.logger.log('supported'); + this.emit('protocol-version', { + message: `Protocol Version ${this.protocolVersion} is supported!`, + errorCode: PROTOCOL_ERROR_CODES.SUPPORTED + }); + } else if (semver.gtr(this.protocolVersion, this.supportedVersionRange)) { + this.logger.log('roku-debug has not been tested against protocol version', this.protocolVersion); + this.emit('protocol-version', { + message: `Protocol Version ${this.protocolVersion} has not been tested and my not work as intended.\nPlease open any issues you have with this version to https://github.com/rokucommunity/roku-debug/issues`, + errorCode: PROTOCOL_ERROR_CODES.NOT_TESTED + }); + } else { + this.logger.log('not supported'); + this.emit('protocol-version', { + message: `Protocol Version ${this.protocolVersion} is not supported.\nIf you believe this is an error please open an issue at https://github.com/rokucommunity/roku-debug/issues`, + errorCode: PROTOCOL_ERROR_CODES.NOT_SUPPORTED + }); + await this.shutdown('close'); + handshakeVerified = false; + } + + this.emit('handshake-verified', handshakeVerified); + return handshakeVerified; + } else { + this.logger.log('Closing connection due to bad debugger magic', response.data.magic); + this.emit('handshake-verified', false); + await this.shutdown('close'); + return false; + } + } + + /** + * When the debugger emits the IOPortOpenedUpdate, we need to immediately connect to the IO port to start receiving that data + */ + private connectToIoPort(update: IOPortOpenedUpdate) { + if (update.success) { + // Create a new TCP client. + this.ioSocket = new Net.Socket({ + allowHalfOpen: false + }); + // Send a connection request to the server. + this.logger.log(`Connect to IO Port ${this.options.host}:${update.data.port}`); + + //sometimes the server shuts down before we had a chance to connect, so recover more gracefully + try { + this.ioSocket.connect({ + port: update.data.port, + host: this.options.host + }, () => { + // If there is no error, the server has accepted the request + this.logger.log('TCP connection established with the IO Port.'); + this.connectedToIoPort = true; + + let lastPartialLine = ''; + this.ioSocket.on('data', (buffer) => { + this.writeToBufferLog('io', buffer); + let responseText = buffer.toString(); + if (!responseText.endsWith('\n')) { + // buffer was split, save the partial line + lastPartialLine += responseText; + } else { + if (lastPartialLine) { + // there was leftover lines, join the partial lines back together + responseText = lastPartialLine + responseText; + lastPartialLine = ''; + } + // Emit the completed io string. + this.emit('io-output', responseText.trim()); + } + }); + + this.ioSocket.on('close', () => { + this.logger.log('IO socket closed'); + this.ioSocketClosed.tryResolve(); + }); + + // Don't forget to catch error, for your own sake. + this.ioSocket.once('error', (err) => { + this.ioSocket.end(); + this.logger.error(err); + }); + }); + return true; + } catch (e) { + this.logger.error(`Failed to connect to IO socket at ${this.options.host}:${update.data.port}`, e); + void this.shutdown('app-exit'); + } + } + return false; + } + + /** + * Destroy this instance, shutting down any sockets or other long-running items and cleaning up. + * @param immediate if true, all sockets are immediately closed and do not gracefully shut down + */ + public async destroy(immediate = false) { + await this.shutdown('close', immediate); + } + + private async shutdown(eventName: 'app-exit' | 'close', immediate = false) { + this.logger.log('Shutting down!'); + + this.cancelControlConnectInterval?.(); + for (const pendingSocket of this.pendingControlConnectionSockets) { + pendingSocket.removeAllListeners(); + //cleanup and destroy all other sockets + if (pendingSocket !== this.controlSocket) { + pendingSocket.end(); + pendingSocket?.destroy(); + } + } + + let exitChannelTimeout = this.options?.exitChannelTimeout ?? 30_000; + let shutdownTimeMax = this.options?.shutdownTimeout ?? 10_000; + //if immediate is true, this is an instant shutdown force. don't wait for anything + if (immediate) { + exitChannelTimeout = 0; + shutdownTimeMax = 0; + } + + //tell the device to exit the channel + try { + //ask the device to terminate the debug session. We have to wait for this to come back. + //The device might be running unstoppable code, so this might take a while. Wait for the device to send back + //the response before we continue with the teardown process + await Promise.race([ + immediate + ? Promise.resolve(null) + : this.exitChannel().finally(() => this.logger.log('exit channel completed')), + //if the exit channel request took this long to finish, something's terribly wrong + util.sleep(exitChannelTimeout) + ]); + } finally { } + + + await Promise.all([ + this.destroyControlSocket(shutdownTimeMax), + this.destroyIOSocket(shutdownTimeMax, immediate) + ]); + this.emit(eventName); + this.emitter?.removeAllListeners(); + this.buffer = Buffer.alloc(0); + this.bufferQueue.destroy(); + } + + private isDestroyingControlSocket = false; + + private async destroyControlSocket(timeout: number) { + if (this.controlSocket && !this.isDestroyingControlSocket) { + this.isDestroyingControlSocket = true; + + //wait for the controlSocket to be closed + await Promise.race([ + this.controlSocketClosed.promise, + util.sleep(timeout) + ]); + + this.logger.log('[destroy] controlSocket is: ', this.controlSocketClosed.isResolved ? 'closed' : 'not closed'); + + //destroy the controlSocket + this.controlSocket.removeAllListeners(); + this.controlSocket.destroy(); + this.controlSocket = undefined; + this.isDestroyingControlSocket = false; + } + } + + private isDestroyingIOSocket = false; + + /** + * @param immediate if true, force close immediately instead of waiting for it to settle + */ + private async destroyIOSocket(timeout: number, immediate = false) { + if (this.ioSocket && !this.isDestroyingIOSocket) { + this.isDestroyingIOSocket = true; + //wait for the ioSocket to be closed + await Promise.race([ + this.ioSocketClosed.promise.then(() => this.logger.log('IO socket closed')), + util.sleep(timeout) + ]); + + //if the io socket is not closed, wait for it to at least settle + if (!this.ioSocketClosed.isCompleted && !immediate) { + await new Promise((resolve) => { + const callback = debounce(() => { + resolve(); + }, 250); + //trigger the current callback once. + callback(); + this.ioSocket?.on('drain', callback as () => void); + }); + } + + this.logger.log('[destroy] ioSocket is: ', this.ioSocketClosed.isResolved ? 'closed' : 'not closed'); + + //destroy the ioSocket + this.ioSocket?.removeAllListeners?.(); + this.ioSocket?.destroy?.(); + this.ioSocket = undefined; + this.isDestroyingIOSocket = false; + } + } +} + +export interface ProtocolVersionDetails { + message: string; + errorCode: PROTOCOL_ERROR_CODES; +} + +export interface BreakpointSpec { + /** + * The path of the source file where the breakpoint is to be inserted. + */ + filePath: string; + /** + * The (1-based) line number in the channel application code where the breakpoint is to be executed. + */ + lineNumber: number; + /** + * The number of times to ignore the breakpoint condition before executing the breakpoint. This number is decremented each time the channel application reaches the breakpoint. + */ + hitCount?: number; + /** + * BrightScript code that evaluates to a boolean value. The expression is compiled and executed in + * the context where the breakpoint is located. If specified, the hitCount is only be + * updated if this evaluates to true. + * @avaiable since protocol version 3.1.0 + */ + conditionalExpression?: string; +} + +export interface ConstructorOptions { + /** + * The host/ip address of the Roku + */ + host: string; + /** + * The port number used to send all debugger commands. This is static/unchanging for Roku devices, + * but is configurable here to support unit testing or alternate runtimes (i.e. https://www.npmjs.com/package/brs) + */ + controlPort?: number; + /** + * The interval (in milliseconds) for how frequently the `connect` + * call should retry connecting to the control port. At the start of a debug session, + * the protocol debugger will start trying to connect the moment the channel is sideloaded, + * and keep trying until a successful connection is established or the debug session is terminated + * @default 250 + */ + controlConnectInterval?: number; + /** + * The maximum time (in milliseconds) the debugger will keep retrying connections. + * This is here to prevent infinitely pinging the Roku device. + */ + controlConnectMaxTime?: number; + + /** + * The number of milliseconds that the client should wait during a shutdown request before forcefully terminating the sockets + */ + shutdownTimeout?: number; + + /** + * The max time the client will wait for the `exit channel` response before forcefully terminating the sockets + */ + exitChannelTimeout?: number; +} + +/** + * Is the event a ProtocolRequest + */ +export function isProtocolRequest(event: ProtocolRequest | ProtocolResponse | ProtocolUpdate): event is ProtocolRequest { + return event?.constructor?.name.endsWith('Request') && event?.data?.requestId > 0; +} + +/** + * Is the event a ProtocolResponse + */ +export function isProtocolResponse(event: ProtocolRequest | ProtocolResponse | ProtocolUpdate): event is ProtocolResponse { + return event?.constructor?.name.endsWith('Response') && event?.data?.requestId !== 0; +} + +/** + * Is the event a ProtocolUpdate update + */ +export function isProtocolUpdate(event: ProtocolRequest | ProtocolResponse | ProtocolUpdate): event is ProtocolUpdate { + return event?.constructor?.name.endsWith('Update') && event?.data?.requestId === 0; +} + +export interface BreakpointsVerifiedEvent { + breakpoints: VerifiedBreakpoint[]; +} diff --git a/src/debugProtocol/client/DebugProtocolClientPlugin.ts b/src/debugProtocol/client/DebugProtocolClientPlugin.ts new file mode 100644 index 00000000..aab44459 --- /dev/null +++ b/src/debugProtocol/client/DebugProtocolClientPlugin.ts @@ -0,0 +1,55 @@ +import type { DebugProtocolClient } from './DebugProtocolClient'; +import type { Socket } from 'net'; +import type { ProtocolRequest, ProtocolResponse, ProtocolUpdate } from '../events/ProtocolEvent'; + +export interface DebugProtocolClientPlugin { + onServerConnected?(event: OnServerConnectedEvent): void | Promise; + + beforeSendRequest?(event: BeforeSendRequestEvent): void | Promise; + afterSendRequest?(event: AfterSendRequestEvent): void | Promise; + + provideResponseOrUpdate?(event: ProvideResponseOrUpdateEvent): void | Promise; + + onUpdate?(event: OnUpdateEvent): void | Promise; + onResponse?(event: OnResponseEvent): void | Promise; + + beforeHandleUpdate?(event: BeforeHandleUpdateEvent): void | Promise; +} + +export interface OnServerConnectedEvent { + client: DebugProtocolClient; + server: Socket; +} + +export interface ProvideResponseOrUpdateEvent { + client: DebugProtocolClient; + activeRequests: Map; + buffer: Readonly; + /** + * The plugin should provide this property + */ + responseOrUpdate?: ProtocolResponse | ProtocolUpdate; +} + +export interface BeforeSendRequestEvent { + client: DebugProtocolClient; + request: TRequest; +} +export type AfterSendRequestEvent = BeforeSendRequestEvent; + +export interface OnUpdateEvent { + client: DebugProtocolClient; + update: ProtocolUpdate; +} + +export interface BeforeHandleUpdateEvent { + client: DebugProtocolClient; + update: ProtocolUpdate; +} + +export interface OnResponseEvent { + client: DebugProtocolClient; + response: ProtocolResponse; +} +export type Handler = (event: T) => R; + diff --git a/src/debugProtocol/events/ProtocolEvent.ts b/src/debugProtocol/events/ProtocolEvent.ts new file mode 100644 index 00000000..3363a59a --- /dev/null +++ b/src/debugProtocol/events/ProtocolEvent.ts @@ -0,0 +1,74 @@ +import type { Command, UpdateType } from '../Constants'; + +export interface ProtocolEvent { + /** + * Was this event successful in parsing/ingesting the data in its constructor + */ + success: boolean; + + /** + * The number of bytes that were read from a buffer if this was a success + */ + readOffset: number; + + /** + * Serialize the current object into the debug protocol's binary format, + * stored in a `Buffer` + */ + toBuffer(): Buffer; + + /** + * Contains the actual event data + */ + data: TData; +} + +/** + * The fields that every ProtocolRequest must have + */ +export interface ProtocolRequestData { + //common props + packetLength: number; + requestId: number; + command: Command; +} +export type ProtocolRequest = ProtocolEvent; + +/** + * The fields that every ProtocolUpdateResponse must have + */ +export interface ProtocolUpdateData { + packetLength: number; + requestId: number; + errorCode: number; + updateType: UpdateType; +} +export type ProtocolUpdate = ProtocolEvent; + +/** + * The fields that every ProtocolResponse must have + */ +export interface ProtocolResponseData { + packetLength: number; + requestId: number; + errorCode: number; + /** + * Data included whenever errorCode > 0. + * @since OS 11.5 + */ + errorData?: ProtocolResponseErrorData; +} +export interface ProtocolResponseErrorData { + /** + * The index of the element in the requested path that exists, but has invalid or unknown value. + * (applies to `VariablesResponse`) + */ + invalidPathIndex?: number; + /** + * The index of the element in path that was not found + * (applies to `VariablesResponse`) + */ + missingKeyIndex?: number; +} +export type ProtocolResponse = ProtocolEvent; + diff --git a/src/debugProtocol/events/requests/AddBreakpointsRequest.spec.ts b/src/debugProtocol/events/requests/AddBreakpointsRequest.spec.ts new file mode 100644 index 00000000..0134dd5d --- /dev/null +++ b/src/debugProtocol/events/requests/AddBreakpointsRequest.spec.ts @@ -0,0 +1,84 @@ +import { expect } from 'chai'; +import { Command } from '../../Constants'; +import { AddBreakpointsRequest } from './AddBreakpointsRequest'; + +describe('AddBreakpointsRequest', () => { + it('serializes and deserializes properly with zero breakpoints', () => { + const command = AddBreakpointsRequest.fromJson({ + requestId: 3, + breakpoints: [] + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.AddBreakpoints, + + breakpoints: [] + }); + + expect( + AddBreakpointsRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 16, // 4 bytes + requestId: 3, // 4 bytes + command: Command.AddBreakpoints, // 4 bytes + + // num_breakpoints // 4 bytes + breakpoints: [] + }); + }); + + it('serializes and deserializes properly with breakpoints', () => { + const command = AddBreakpointsRequest.fromJson({ + requestId: 3, + breakpoints: [{ + filePath: 'source/main.brs', + ignoreCount: 3, + lineNumber: 1 + }, + { + filePath: 'source/main.brs', + ignoreCount: undefined, //we default to 0 + lineNumber: 2 + }] + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.AddBreakpoints, + + breakpoints: [{ + filePath: 'source/main.brs', + ignoreCount: 3, + lineNumber: 1 + }, + { + filePath: 'source/main.brs', + ignoreCount: 0, + lineNumber: 2 + }] + }); + + expect( + AddBreakpointsRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 64, // 4 bytes + requestId: 3, // 4 bytes + command: Command.AddBreakpoints, // 4 bytes + + // num_breakpoints // 4 bytes + breakpoints: [{ + filePath: 'source/main.brs', // 16 bytes + ignoreCount: 3, // 4 bytes + lineNumber: 1 // 4 bytes + }, + { + filePath: 'source/main.brs', // 16 bytes + ignoreCount: 0, // 4 bytes + lineNumber: 2 // 4 bytes + }] + }); + }); +}); diff --git a/src/debugProtocol/events/requests/AddBreakpointsRequest.ts b/src/debugProtocol/events/requests/AddBreakpointsRequest.ts new file mode 100644 index 00000000..b2bda891 --- /dev/null +++ b/src/debugProtocol/events/requests/AddBreakpointsRequest.ts @@ -0,0 +1,76 @@ +import { SmartBuffer } from 'smart-buffer'; +import { Command } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolRequest } from '../ProtocolEvent'; + +export class AddBreakpointsRequest implements ProtocolRequest { + + public static fromJson(data: { + requestId: number; + breakpoints: Array<{ + filePath: string; + lineNumber: number; + ignoreCount: number; + }>; + }) { + const request = new AddBreakpointsRequest(); + protocolUtil.loadJson(request, data); + request.data.breakpoints ??= []; + //default ignoreCount to 0 for consistency purposes + for (const breakpoint of request.data.breakpoints) { + breakpoint.ignoreCount ??= 0; + } + return request; + } + + public static fromBuffer(buffer: Buffer) { + const request = new AddBreakpointsRequest(); + protocolUtil.bufferLoaderHelper(request, buffer, 12, (smartBuffer) => { + protocolUtil.loadCommonRequestFields(request, smartBuffer); + + const numBreakpoints = smartBuffer.readUInt32LE(); // num_breakpoints + request.data.breakpoints = []; + for (let i = 0; i < numBreakpoints; i++) { + request.data.breakpoints.push({ + filePath: protocolUtil.readStringNT(smartBuffer), // file_path + lineNumber: smartBuffer.readUInt32LE(), // line_number + ignoreCount: smartBuffer.readUInt32LE() // ignore_count + }); + } + }); + return request; + } + + public toBuffer(): Buffer { + const smartBuffer = new SmartBuffer(); + + smartBuffer.writeUInt32LE(this.data.breakpoints.length); // num_breakpoints + for (const breakpoint of this.data.breakpoints) { + smartBuffer.writeStringNT(breakpoint.filePath); // file_path + smartBuffer.writeUInt32LE(breakpoint.lineNumber); // line_number + smartBuffer.writeUInt32LE(breakpoint.ignoreCount); // ignore_count + } + + protocolUtil.insertCommonRequestFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + breakpoints: undefined as Array<{ + filePath: string; + lineNumber: number; + ignoreCount: number; + }>, + + //common props + packetLength: undefined as number, + requestId: undefined as number, + command: Command.AddBreakpoints + }; +} diff --git a/src/debugProtocol/events/requests/AddConditionalBreakpointsRequest.spec.ts b/src/debugProtocol/events/requests/AddConditionalBreakpointsRequest.spec.ts new file mode 100644 index 00000000..7509eeb3 --- /dev/null +++ b/src/debugProtocol/events/requests/AddConditionalBreakpointsRequest.spec.ts @@ -0,0 +1,91 @@ +import { expect } from 'chai'; +import { Command } from '../../Constants'; +import { AddConditionalBreakpointsRequest } from './AddConditionalBreakpointsRequest'; + +describe('AddConditionalBreakpointsRequest', () => { + it('serializes and deserializes properly with zero breakpoints', () => { + const command = AddConditionalBreakpointsRequest.fromJson({ + requestId: 3, + breakpoints: [] + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.AddConditionalBreakpoints, + + breakpoints: [] + }); + + expect( + AddConditionalBreakpointsRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 20, // 4 bytes + requestId: 3, // 4 bytes + command: Command.AddConditionalBreakpoints, // 4 bytes + // flags // 4 bytes + // num_breakpoints // 4 bytes + breakpoints: [] + }); + }); + + it('serializes and deserializes properly with breakpoints', () => { + const command = AddConditionalBreakpointsRequest.fromJson({ + requestId: 3, + breakpoints: [{ + filePath: 'source/main.brs', + ignoreCount: 3, + lineNumber: 1, + conditionalExpression: '1=1' + }, + { + filePath: 'source/main.brs', + ignoreCount: undefined, //we default to 0 + lineNumber: 2 + }] + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.AddConditionalBreakpoints, + + breakpoints: [{ + filePath: 'source/main.brs', + ignoreCount: 3, + lineNumber: 1, + conditionalExpression: '1=1' + }, + { + filePath: 'source/main.brs', + ignoreCount: 0, + lineNumber: 2, + conditionalExpression: 'true' + }] + }); + + expect( + AddConditionalBreakpointsRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 77, // 4 bytes + requestId: 3, // 4 bytes + command: Command.AddConditionalBreakpoints, // 4 bytes + + //flags // 4 bytes + + // num_breakpoints // 4 bytes + breakpoints: [{ + filePath: 'source/main.brs', // 16 bytes + ignoreCount: 3, // 4 bytes + lineNumber: 1, // 4 bytes + conditionalExpression: '1=1' // 4 bytes + }, + { + filePath: 'source/main.brs', // 16 bytes + ignoreCount: 0, // 4 bytes + lineNumber: 2, // 4 bytes + conditionalExpression: 'true' // 5 bytes + }] + }); + }); +}); diff --git a/src/debugProtocol/events/requests/AddConditionalBreakpointsRequest.ts b/src/debugProtocol/events/requests/AddConditionalBreakpointsRequest.ts new file mode 100644 index 00000000..5854bbef --- /dev/null +++ b/src/debugProtocol/events/requests/AddConditionalBreakpointsRequest.ts @@ -0,0 +1,103 @@ +import { SmartBuffer } from 'smart-buffer'; +import { util } from '../../../util'; +import { Command } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolRequest } from '../ProtocolEvent'; + +export class AddConditionalBreakpointsRequest implements ProtocolRequest { + + public static fromJson(data: { + requestId: number; + breakpoints: Array<{ + filePath: string; + lineNumber: number; + ignoreCount: number; + conditionalExpression?: string; + }>; + }) { + const request = new AddConditionalBreakpointsRequest(); + protocolUtil.loadJson(request, data); + request.data.breakpoints ??= []; + //default ignoreCount to 0 for consistency purposes + for (const breakpoint of request.data.breakpoints) { + breakpoint.ignoreCount ??= 0; + //There's a bug in 3.1 where empty conditional expressions would crash the breakpoints, so just default to `true` which always succeeds + breakpoint.conditionalExpression = breakpoint.conditionalExpression?.trim() ? breakpoint.conditionalExpression : 'true'; + } + return request; + } + + public static fromBuffer(buffer: Buffer) { + const request = new AddConditionalBreakpointsRequest(); + protocolUtil.bufferLoaderHelper(request, buffer, 12, (smartBuffer) => { + protocolUtil.loadCommonRequestFields(request, smartBuffer); + + smartBuffer.readUInt32LE(); // flags - Should always be passed as 0. Unused, reserved for future use. + + const numBreakpoints = smartBuffer.readUInt32LE(); // num_breakpoints + request.data.breakpoints = []; + for (let i = 0; i < numBreakpoints; i++) { + request.data.breakpoints.push({ + filePath: protocolUtil.readStringNT(smartBuffer), // file_path + lineNumber: smartBuffer.readUInt32LE(), // line_number + ignoreCount: smartBuffer.readUInt32LE(), // ignore_count + conditionalExpression: protocolUtil.readStringNT(smartBuffer) // cond_expr + }); + } + }); + return request; + } + + public toBuffer(): Buffer { + const smartBuffer = new SmartBuffer(); + + smartBuffer.writeUInt32LE(0); // flags - Should always be passed as 0. Unused, reserved for future use. + + smartBuffer.writeUInt32LE(this.data.breakpoints.length); // num_breakpoints + for (const breakpoint of this.data.breakpoints) { + smartBuffer.writeStringNT(breakpoint.filePath); // file_path + smartBuffer.writeUInt32LE(breakpoint.lineNumber); // line_number + smartBuffer.writeUInt32LE(breakpoint.ignoreCount); // ignore_count + smartBuffer.writeStringNT(breakpoint.conditionalExpression); // cond_expr + } + + protocolUtil.insertCommonRequestFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + breakpoints: undefined as Array<{ + /** + * The path of the source file where the conditional breakpoint is to be inserted. + * + * "pkg:/" specifies a file in the channel + * + * "lib://" specifies a file in a library. + */ + filePath: string; + /** + * The line number in the channel application code where the breakpoint is to be executed. + */ + lineNumber: number; + /** + * The number of times to ignore the breakpoint condition before executing the breakpoint. This number is decremented each time the channel application reaches the breakpoint. If cond_expr is specified, the ignore_count is only updated if it evaluates to true. + */ + ignoreCount: number; + /** + * BrightScript code that evaluates to a boolean value. The cond_expr is compiled and executed in the context where the breakpoint is located. If cond_expr is specified, the ignore_count is only be updated if this evaluates to true. + */ + conditionalExpression?: string; + }>, + + //common props + packetLength: undefined as number, + requestId: undefined as number, + command: Command.AddConditionalBreakpoints + }; +} diff --git a/src/debugProtocol/events/requests/ContinueRequest.spec.ts b/src/debugProtocol/events/requests/ContinueRequest.spec.ts new file mode 100644 index 00000000..f0c4cde1 --- /dev/null +++ b/src/debugProtocol/events/requests/ContinueRequest.spec.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import { Command } from '../../Constants'; +import { ContinueRequest } from './ContinueRequest'; + +describe('ContinueRequest', () => { + it('serializes and deserializes properly', () => { + const command = ContinueRequest.fromJson({ + requestId: 3 + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.Continue + }); + + expect( + ContinueRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 12, // 4 bytes + requestId: 3, // 4 bytes + command: Command.Continue // 4 bytes + }); + }); +}); diff --git a/src/debugProtocol/events/requests/ContinueRequest.ts b/src/debugProtocol/events/requests/ContinueRequest.ts new file mode 100644 index 00000000..6ea31745 --- /dev/null +++ b/src/debugProtocol/events/requests/ContinueRequest.ts @@ -0,0 +1,40 @@ +import { SmartBuffer } from 'smart-buffer'; +import { Command } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolRequest } from '../ProtocolEvent'; + +export class ContinueRequest implements ProtocolRequest { + + public static fromJson(data: { requestId: number }) { + const request = new ContinueRequest(); + protocolUtil.loadJson(request, data); + return request; + } + + public static fromBuffer(buffer: Buffer) { + const request = new ContinueRequest(); + protocolUtil.bufferLoaderHelper(request, buffer, 12, (smartBuffer) => { + protocolUtil.loadCommonRequestFields(request, smartBuffer); + }); + return request; + } + + public toBuffer(): Buffer { + const smartBuffer = new SmartBuffer(); + protocolUtil.insertCommonRequestFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + //common props + packetLength: undefined as number, + requestId: undefined as number, + command: Command.Continue + }; +} diff --git a/src/debugProtocol/events/requests/ExecuteRequest.spec.ts b/src/debugProtocol/events/requests/ExecuteRequest.spec.ts new file mode 100644 index 00000000..8afca064 --- /dev/null +++ b/src/debugProtocol/events/requests/ExecuteRequest.spec.ts @@ -0,0 +1,36 @@ +import { expect } from 'chai'; +import { Command } from '../../Constants'; +import { ExecuteRequest } from './ExecuteRequest'; + +describe('ExecuteRequest', () => { + it('serializes and deserializes properly', () => { + const command = ExecuteRequest.fromJson({ + requestId: 3, + sourceCode: 'print "text"', + stackFrameIndex: 2, + threadIndex: 1 + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.Execute, + + sourceCode: 'print "text"', + stackFrameIndex: 2, + threadIndex: 1 + }); + + expect( + ExecuteRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 33, // 4 bytes + requestId: 3, // 4 bytes + command: Command.Execute, // 4 bytes + + sourceCode: 'print "text"', // 13 bytes + stackFrameIndex: 2, // 4 bytes + threadIndex: 1 // 4 bytes + }); + }); +}); diff --git a/src/debugProtocol/events/requests/ExecuteRequest.ts b/src/debugProtocol/events/requests/ExecuteRequest.ts new file mode 100644 index 00000000..55abc68a --- /dev/null +++ b/src/debugProtocol/events/requests/ExecuteRequest.ts @@ -0,0 +1,58 @@ +import { SmartBuffer } from 'smart-buffer'; +import { Command } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolRequest } from '../ProtocolEvent'; + +export class ExecuteRequest implements ProtocolRequest { + + public static fromJson(data: { + requestId: number; + threadIndex: number; + stackFrameIndex: number; + sourceCode: string; + }) { + const request = new ExecuteRequest(); + protocolUtil.loadJson(request, data); + return request; + } + + public static fromBuffer(buffer: Buffer) { + const request = new ExecuteRequest(); + protocolUtil.bufferLoaderHelper(request, buffer, 12, (smartBuffer) => { + protocolUtil.loadCommonRequestFields(request, smartBuffer); + + request.data.threadIndex = smartBuffer.readUInt32LE(); // thread_index + request.data.stackFrameIndex = smartBuffer.readUInt32LE(); // stack_frame_index + request.data.sourceCode = protocolUtil.readStringNT(smartBuffer); // source_code + }); + return request; + } + + public toBuffer(): Buffer { + const smartBuffer = new SmartBuffer(); + + smartBuffer.writeUInt32LE(this.data.threadIndex); // thread_index + smartBuffer.writeUInt32LE(this.data.stackFrameIndex); // stack_frame_index + smartBuffer.writeStringNT(this.data.sourceCode); // source_code + + protocolUtil.insertCommonRequestFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + threadIndex: undefined as number, + stackFrameIndex: undefined as number, + sourceCode: undefined as string, + + //common props + packetLength: undefined as number, + requestId: undefined as number, + command: Command.Execute + }; +} diff --git a/src/debugProtocol/events/requests/ExitChannelRequest.spec.ts b/src/debugProtocol/events/requests/ExitChannelRequest.spec.ts new file mode 100644 index 00000000..78982e56 --- /dev/null +++ b/src/debugProtocol/events/requests/ExitChannelRequest.spec.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import { Command } from '../../Constants'; +import { ExitChannelRequest } from './ExitChannelRequest'; + +describe('ExitChannelRequest', () => { + it('serializes and deserializes properly', () => { + const command = ExitChannelRequest.fromJson({ + requestId: 3 + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.ExitChannel + }); + + expect( + ExitChannelRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 12, // 4 bytes + requestId: 3, // 4 bytes + command: Command.ExitChannel // 4 bytes + }); + }); +}); diff --git a/src/debugProtocol/events/requests/ExitChannelRequest.ts b/src/debugProtocol/events/requests/ExitChannelRequest.ts new file mode 100644 index 00000000..3c5b0abd --- /dev/null +++ b/src/debugProtocol/events/requests/ExitChannelRequest.ts @@ -0,0 +1,40 @@ +import { SmartBuffer } from 'smart-buffer'; +import { Command } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolRequest } from '../ProtocolEvent'; + +export class ExitChannelRequest implements ProtocolRequest { + + public static fromJson(data: { requestId: number }) { + const request = new ExitChannelRequest(); + protocolUtil.loadJson(request, data); + return request; + } + + public static fromBuffer(buffer: Buffer) { + const request = new ExitChannelRequest(); + protocolUtil.bufferLoaderHelper(request, buffer, 12, (smartBuffer) => { + protocolUtil.loadCommonRequestFields(request, smartBuffer); + }); + return request; + } + + public toBuffer(): Buffer { + const smartBuffer = new SmartBuffer(); + protocolUtil.insertCommonRequestFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + //common props + packetLength: undefined as number, + requestId: undefined as number, + command: Command.ExitChannel + }; +} diff --git a/src/debugProtocol/events/requests/HandshakeRequest.spec.ts b/src/debugProtocol/events/requests/HandshakeRequest.spec.ts new file mode 100644 index 00000000..cec6731f --- /dev/null +++ b/src/debugProtocol/events/requests/HandshakeRequest.spec.ts @@ -0,0 +1,31 @@ +import { expect } from 'chai'; +import { HandshakeRequest } from './HandshakeRequest'; + +describe('HandshakeRequest', () => { + it('serializes and deserializes properly', () => { + let request = HandshakeRequest.fromJson({ + magic: 'theMagic!' + }); + + expect(request.data).to.eql({ + packetLength: undefined, + requestId: HandshakeRequest.REQUEST_ID, + command: undefined, + + magic: 'theMagic!' + }); + + request = HandshakeRequest.fromBuffer(request.toBuffer()); + expect(request.readOffset).to.eql(10); + + expect( + request.data + ).to.eql({ + packetLength: undefined, + requestId: HandshakeRequest.REQUEST_ID, + command: undefined, + + magic: 'theMagic!' + }); + }); +}); diff --git a/src/debugProtocol/events/requests/HandshakeRequest.ts b/src/debugProtocol/events/requests/HandshakeRequest.ts new file mode 100644 index 00000000..2335b5d1 --- /dev/null +++ b/src/debugProtocol/events/requests/HandshakeRequest.ts @@ -0,0 +1,51 @@ +import { SmartBuffer } from 'smart-buffer'; +import { util } from '../../../util'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolRequest } from '../ProtocolEvent'; +import type { Command } from '../../Constants'; + +/** + * The initial handshake sent by the client. This is just the `magic` to initiate the debug protocol session + * @since protocol v1.0.0 + */ +export class HandshakeRequest implements ProtocolRequest { + /** + * A hardcoded id for the handshake classes to help them flow through the request/response flow even though Handshake events don't look the same as other protocol events + */ + public static REQUEST_ID = 4294967295; + + public static fromJson(data: { magic: string }) { + const request = new HandshakeRequest(); + protocolUtil.loadJson(request, data); + return request; + } + + public static fromBuffer(buffer: Buffer) { + const request = new HandshakeRequest(); + protocolUtil.bufferLoaderHelper(request, buffer, 0, (smartBuffer) => { + request.data.magic = protocolUtil.readStringNT(smartBuffer); + }); + return request; + } + + public toBuffer() { + return new SmartBuffer({ + size: Buffer.byteLength(this.data.magic) + 1 + }).writeStringNT(this.data.magic).toBuffer(); + } + + public success = false; + + public readOffset = undefined; + + public data = { + magic: undefined as string, + + //handshake requests aren't actually structured like like normal requests, but since they're the only unique type of request, + //just add dummy data for those fields + packetLength: undefined as number, + //hardcode the max integer value. This must be the same value as the HandshakeResponse class + requestId: HandshakeRequest.REQUEST_ID, + command: undefined as Command + }; +} diff --git a/src/debugProtocol/events/requests/ListBreakpointsRequest.spec.ts b/src/debugProtocol/events/requests/ListBreakpointsRequest.spec.ts new file mode 100644 index 00000000..937c5c5e --- /dev/null +++ b/src/debugProtocol/events/requests/ListBreakpointsRequest.spec.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import { Command } from '../../Constants'; +import { ListBreakpointsRequest } from './ListBreakpointsRequest'; + +describe('ListBreakpointsRequest', () => { + it('serializes and deserializes properly', () => { + const command = ListBreakpointsRequest.fromJson({ + requestId: 3 + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.ListBreakpoints + }); + + expect( + ListBreakpointsRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 12, // 4 bytes + requestId: 3, // 4 bytes + command: Command.ListBreakpoints // 4 bytes + }); + }); +}); diff --git a/src/debugProtocol/events/requests/ListBreakpointsRequest.ts b/src/debugProtocol/events/requests/ListBreakpointsRequest.ts new file mode 100644 index 00000000..fc6458b5 --- /dev/null +++ b/src/debugProtocol/events/requests/ListBreakpointsRequest.ts @@ -0,0 +1,40 @@ +import { SmartBuffer } from 'smart-buffer'; +import { Command } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolRequest } from '../ProtocolEvent'; + +export class ListBreakpointsRequest implements ProtocolRequest { + + public static fromJson(data: { requestId: number }) { + const request = new ListBreakpointsRequest(); + protocolUtil.loadJson(request, data); + return request; + } + + public static fromBuffer(buffer: Buffer) { + const request = new ListBreakpointsRequest(); + protocolUtil.bufferLoaderHelper(request, buffer, 12, (smartBuffer) => { + protocolUtil.loadCommonRequestFields(request, smartBuffer); + }); + return request; + } + + public toBuffer(): Buffer { + const smartBuffer = new SmartBuffer(); + protocolUtil.insertCommonRequestFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + //common props + packetLength: undefined as number, + requestId: undefined as number, + command: Command.ListBreakpoints + }; +} diff --git a/src/debugProtocol/events/requests/RemoveBreakpointsCommand.spec.ts b/src/debugProtocol/events/requests/RemoveBreakpointsCommand.spec.ts new file mode 100644 index 00000000..76da8c1b --- /dev/null +++ b/src/debugProtocol/events/requests/RemoveBreakpointsCommand.spec.ts @@ -0,0 +1,32 @@ +import { expect } from 'chai'; +import { Command } from '../../Constants'; +import { RemoveBreakpointsRequest } from './RemoveBreakpointsRequest'; + +describe('RemoveBreakpointsRequest', () => { + it('serializes and deserializes properly', () => { + const command = RemoveBreakpointsRequest.fromJson({ + requestId: 3, + breakpointIds: [1, 2, 100] + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.RemoveBreakpoints, + + breakpointIds: [1, 2, 100], + numBreakpoints: 3 + }); + + expect( + RemoveBreakpointsRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 28, // 4 bytes + requestId: 3, // 4 bytes + command: Command.RemoveBreakpoints, // 4 bytes + + breakpointIds: [1, 2, 100], // 12 bytes + numBreakpoints: 3 // 4 bytes + }); + }); +}); diff --git a/src/debugProtocol/events/requests/RemoveBreakpointsRequest.ts b/src/debugProtocol/events/requests/RemoveBreakpointsRequest.ts new file mode 100644 index 00000000..2cb5604a --- /dev/null +++ b/src/debugProtocol/events/requests/RemoveBreakpointsRequest.ts @@ -0,0 +1,62 @@ +import { SmartBuffer } from 'smart-buffer'; +import { Command } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolRequest } from '../ProtocolEvent'; + +export class RemoveBreakpointsRequest implements ProtocolRequest { + + public static fromJson(data: { requestId: number; breakpointIds: number[] }) { + const request = new RemoveBreakpointsRequest(); + protocolUtil.loadJson(request, data); + request.data.numBreakpoints = request.data.breakpointIds?.length ?? 0; + return request; + } + + public static fromBuffer(buffer: Buffer) { + const command = new RemoveBreakpointsRequest(); + protocolUtil.bufferLoaderHelper(command, buffer, 16, (smartBuffer) => { + protocolUtil.loadCommonRequestFields(command, smartBuffer); + command.data.numBreakpoints = smartBuffer.readUInt32LE(); + command.data.breakpointIds = []; + for (let i = 0; i < command.data.numBreakpoints; i++) { + command.data.breakpointIds.push( + smartBuffer.readUInt32LE() + ); + } + }); + return command; + } + + public toBuffer() { + const smartBuffer = new SmartBuffer(); + smartBuffer.writeUInt32LE(this.data.breakpointIds?.length ?? 0); // num_breakpoints + for (const breakpointId of this.data.breakpointIds ?? []) { + smartBuffer.writeUInt32LE(breakpointId as number); // breakpoint_ids + } + protocolUtil.insertCommonRequestFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + /** + * The number of breakpoints in the breakpoints array. + */ + numBreakpoints: undefined as number, + /** + * An array of breakpoint IDs representing the breakpoints to be removed. + */ + breakpointIds: [], + + + //common props + packetLength: undefined as number, + requestId: undefined as number, + command: Command.RemoveBreakpoints + }; +} diff --git a/src/debugProtocol/events/requests/StackTraceRequest.spec.ts b/src/debugProtocol/events/requests/StackTraceRequest.spec.ts new file mode 100644 index 00000000..e7b6a2fc --- /dev/null +++ b/src/debugProtocol/events/requests/StackTraceRequest.spec.ts @@ -0,0 +1,30 @@ +import { expect } from 'chai'; +import { Command, StepType } from '../../Constants'; +import { StackTraceRequest } from './StackTraceRequest'; + +describe('StackTraceRequest', () => { + it('serializes and deserializes properly', () => { + const command = StackTraceRequest.fromJson({ + requestId: 3, + threadIndex: 2 + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.StackTrace, + + threadIndex: 2 + }); + + expect( + StackTraceRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 16, // 4 bytes + requestId: 3, // 4 bytes + command: Command.StackTrace, // 4 bytes + + threadIndex: 2 //4 bytes + }); + }); +}); diff --git a/src/debugProtocol/events/requests/StackTraceRequest.ts b/src/debugProtocol/events/requests/StackTraceRequest.ts new file mode 100644 index 00000000..8e4d1380 --- /dev/null +++ b/src/debugProtocol/events/requests/StackTraceRequest.ts @@ -0,0 +1,47 @@ +import { SmartBuffer } from 'smart-buffer'; +import { Command } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolRequest } from '../ProtocolEvent'; + +export class StackTraceRequest implements ProtocolRequest { + + public static fromJson(data: { requestId: number; threadIndex: number }) { + const request = new StackTraceRequest(); + protocolUtil.loadJson(request, data); + return request; + } + + public static fromBuffer(buffer: Buffer) { + const request = new StackTraceRequest(); + protocolUtil.bufferLoaderHelper(request, buffer, 12, (smartBuffer) => { + protocolUtil.loadCommonRequestFields(request, smartBuffer); + request.data.threadIndex = smartBuffer.readUInt32LE(); //thread_index + }); + return request; + } + + public toBuffer(): Buffer { + const smartBuffer = new SmartBuffer(); + + smartBuffer.writeUInt32LE(this.data.threadIndex); //thread_index + + protocolUtil.insertCommonRequestFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + threadIndex: undefined as number, + + //common props + packetLength: undefined as number, + requestId: undefined as number, + command: Command.StackTrace + }; +} + diff --git a/src/debugProtocol/events/requests/StepRequest.spec.ts b/src/debugProtocol/events/requests/StepRequest.spec.ts new file mode 100644 index 00000000..c068bd69 --- /dev/null +++ b/src/debugProtocol/events/requests/StepRequest.spec.ts @@ -0,0 +1,33 @@ +import { expect } from 'chai'; +import { Command, StepType } from '../../Constants'; +import { StepRequest } from './StepRequest'; + +describe('StepRequest', () => { + it('serializes and deserializes properly', () => { + const command = StepRequest.fromJson({ + requestId: 3, + threadIndex: 2, + stepType: StepType.Line + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.Step, + + threadIndex: 2, + stepType: StepType.Line + }); + + expect( + StepRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 17, // 4 bytes + requestId: 3, // 4 bytes + command: Command.Step, // 4 bytes + + stepType: StepType.Line, // 1 byte + threadIndex: 2 // 4 bytes + }); + }); +}); diff --git a/src/debugProtocol/events/requests/StepRequest.ts b/src/debugProtocol/events/requests/StepRequest.ts new file mode 100644 index 00000000..89d5916d --- /dev/null +++ b/src/debugProtocol/events/requests/StepRequest.ts @@ -0,0 +1,52 @@ +import { SmartBuffer } from 'smart-buffer'; +import type { StepType } from '../../Constants'; +import { Command, StepTypeCode } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolRequest } from '../ProtocolEvent'; + +export class StepRequest implements ProtocolRequest { + + public static fromJson(data: { requestId: number; threadIndex: number; stepType: StepType }) { + const request = new StepRequest(); + protocolUtil.loadJson(request, data); + return request; + } + + public static fromBuffer(buffer: Buffer) { + const request = new StepRequest(); + protocolUtil.bufferLoaderHelper(request, buffer, 12, (smartBuffer) => { + protocolUtil.loadCommonRequestFields(request, smartBuffer); + request.data.threadIndex = smartBuffer.readUInt32LE(); // thread_index + request.data.stepType = StepTypeCode[smartBuffer.readUInt8()] as StepType; // step_type + }); + return request; + } + + public toBuffer(): Buffer { + const smartBuffer = new SmartBuffer(); + + smartBuffer.writeUInt32LE(this.data.threadIndex); //thread_index + smartBuffer.writeUInt8(StepTypeCode[this.data.stepType]); //step_type + + protocolUtil.insertCommonRequestFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + threadIndex: undefined as number, + stepType: undefined as StepType, + + //common props + command: Command.Step, + packetLength: undefined as number, + requestId: undefined as number + + }; +} + diff --git a/src/debugProtocol/events/requests/StopRequest.spec.ts b/src/debugProtocol/events/requests/StopRequest.spec.ts new file mode 100644 index 00000000..384841fb --- /dev/null +++ b/src/debugProtocol/events/requests/StopRequest.spec.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import { Command, StepType } from '../../Constants'; +import { StopRequest } from './StopRequest'; + +describe('StopRequest', () => { + it('serializes and deserializes properly', () => { + const command = StopRequest.fromJson({ + requestId: 3 + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.Stop + }); + + expect( + StopRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 12, // 4 bytes + requestId: 3, // 4 bytes + command: Command.Stop // 4 bytes + }); + }); +}); diff --git a/src/debugProtocol/events/requests/StopRequest.ts b/src/debugProtocol/events/requests/StopRequest.ts new file mode 100644 index 00000000..ef8551f9 --- /dev/null +++ b/src/debugProtocol/events/requests/StopRequest.ts @@ -0,0 +1,40 @@ +import { SmartBuffer } from 'smart-buffer'; +import { Command } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolRequest } from '../ProtocolEvent'; + +export class StopRequest implements ProtocolRequest { + + public static fromJson(data: { requestId: number }) { + const request = new StopRequest(); + protocolUtil.loadJson(request, data); + return request; + } + + public static fromBuffer(buffer: Buffer) { + const request = new StopRequest(); + protocolUtil.bufferLoaderHelper(request, buffer, 12, (smartBuffer) => { + protocolUtil.loadCommonRequestFields(request, smartBuffer); + }); + return request; + } + + public toBuffer(): Buffer { + const smartBuffer = new SmartBuffer(); + protocolUtil.insertCommonRequestFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + packetLength: undefined as number, + requestId: undefined as number, + command: Command.Stop + }; +} diff --git a/src/debugProtocol/events/requests/ThreadsRequest.spec.ts b/src/debugProtocol/events/requests/ThreadsRequest.spec.ts new file mode 100644 index 00000000..d9c835c9 --- /dev/null +++ b/src/debugProtocol/events/requests/ThreadsRequest.spec.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import { Command } from '../../Constants'; +import { ThreadsRequest } from './ThreadsRequest'; + +describe('ThreadsRequest', () => { + it('serializes and deserializes properly', () => { + const command = ThreadsRequest.fromJson({ + requestId: 3 + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.Threads + }); + + expect( + ThreadsRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 12, // 4 bytes + requestId: 3, // 4 bytes + command: Command.Threads // 4 bytes + }); + }); +}); diff --git a/src/debugProtocol/events/requests/ThreadsRequest.ts b/src/debugProtocol/events/requests/ThreadsRequest.ts new file mode 100644 index 00000000..f5f577d2 --- /dev/null +++ b/src/debugProtocol/events/requests/ThreadsRequest.ts @@ -0,0 +1,40 @@ +import { SmartBuffer } from 'smart-buffer'; +import { Command } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolRequest } from '../ProtocolEvent'; + +export class ThreadsRequest implements ProtocolRequest { + + public static fromJson(data: { requestId: number }) { + const request = new ThreadsRequest(); + protocolUtil.loadJson(request, data); + return request; + } + + public static fromBuffer(buffer: Buffer) { + const request = new ThreadsRequest(); + protocolUtil.bufferLoaderHelper(request, buffer, 12, (smartBuffer) => { + protocolUtil.loadCommonRequestFields(request, smartBuffer); + }); + return request; + } + + public toBuffer(): Buffer { + const smartBuffer = new SmartBuffer(); + protocolUtil.insertCommonRequestFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + //common props + packetLength: undefined as number, + requestId: undefined as number, + command: Command.Threads + }; +} diff --git a/src/debugProtocol/events/requests/VariablesRequest.spec.ts b/src/debugProtocol/events/requests/VariablesRequest.spec.ts new file mode 100644 index 00000000..a852bb6e --- /dev/null +++ b/src/debugProtocol/events/requests/VariablesRequest.spec.ts @@ -0,0 +1,155 @@ +import { expect } from 'chai'; +import { Command } from '../../Constants'; +import { VariablesRequest } from './VariablesRequest'; + +describe('VariablesRequest', () => { + it('serializes and deserializes properly for unsupported forceCaseSensitivity lookups', () => { + const command = VariablesRequest.fromJson({ + requestId: 3, + getChildKeys: true, + enableForceCaseInsensitivity: false, + stackFrameIndex: 1, + threadIndex: 2, + variablePathEntries: [ + { name: 'a', forceCaseInsensitive: true }, + { name: 'b', forceCaseInsensitive: true }, + { name: 'c', forceCaseInsensitive: true } + ] + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.Variables, + + getChildKeys: true, + enableForceCaseInsensitivity: false, + stackFrameIndex: 1, + threadIndex: 2, + variablePathEntries: [ + { name: 'a', forceCaseInsensitive: false }, + { name: 'b', forceCaseInsensitive: false }, + { name: 'c', forceCaseInsensitive: false } + ] + }); + + expect( + VariablesRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 31, // 4 bytes + requestId: 3, // 4 bytes + command: Command.Variables, // 4 bytes, + + //variable_request_flags // 1 byte + getChildKeys: true, // 0 bytes + enableForceCaseInsensitivity: false, // 0 bytes + stackFrameIndex: 1, // 4 bytes + threadIndex: 2, // 4 bytes + // variable_path_len // 4 bytes + variablePathEntries: [ + { name: 'a', forceCaseInsensitive: false }, // 2 bytes + { name: 'b', forceCaseInsensitive: false }, // 2 bytes + { name: 'c', forceCaseInsensitive: false } // 2 bytes + ] + }); + }); + + it('serializes and deserializes properly for case insensitivesensitive lookups', () => { + const command = VariablesRequest.fromJson({ + requestId: 3, + getChildKeys: false, + enableForceCaseInsensitivity: true, + stackFrameIndex: 1, + threadIndex: 2, + variablePathEntries: [ + { name: 'a', forceCaseInsensitive: true }, + { name: 'b', forceCaseInsensitive: false }, + { name: 'c', forceCaseInsensitive: true } + ] + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.Variables, + + getChildKeys: false, + enableForceCaseInsensitivity: true, + stackFrameIndex: 1, + threadIndex: 2, + variablePathEntries: [ + { name: 'a', forceCaseInsensitive: true }, + { name: 'b', forceCaseInsensitive: false }, + { name: 'c', forceCaseInsensitive: true } + ] + }); + + expect( + VariablesRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 34, // 4 bytes + requestId: 3, // 4 bytes + command: Command.Variables, // 4 bytes, + + //variable_request_flags // 1 byte + getChildKeys: false, // 0 bytes + enableForceCaseInsensitivity: true, // 0 bytes + stackFrameIndex: 1, // 4 bytes + threadIndex: 2, // 4 bytes + // variable_path_len // 4 bytes + variablePathEntries: [ + { + name: 'a', // 2 bytes + forceCaseInsensitive: true // 1 byte + }, // ? + { + name: 'b', // 2 bytes + forceCaseInsensitive: false // 1 byte + }, // ? + { + name: 'c', // 2 bytes + forceCaseInsensitive: true // 1 byte + } // ? + ] + }); + }); + + it('supports empty variables list', () => { + const command = VariablesRequest.fromJson({ + requestId: 3, + getChildKeys: false, + enableForceCaseInsensitivity: true, + stackFrameIndex: 1, + threadIndex: 2, + variablePathEntries: [] + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.Variables, + + getChildKeys: false, + enableForceCaseInsensitivity: true, + stackFrameIndex: 1, + threadIndex: 2, + variablePathEntries: [] + }); + + expect( + VariablesRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 25, // 4 bytes + requestId: 3, // 4 bytes + command: Command.Variables, // 4 bytes, + + //variable_request_flags // 1 byte + getChildKeys: false, // 0 bytes + enableForceCaseInsensitivity: true, // 0 bytes + stackFrameIndex: 1, // 4 bytes + threadIndex: 2, // 4 bytes + // variable_path_len // 4 bytes + variablePathEntries: [] + }); + }); +}); diff --git a/src/debugProtocol/events/requests/VariablesRequest.ts b/src/debugProtocol/events/requests/VariablesRequest.ts new file mode 100644 index 00000000..e5081432 --- /dev/null +++ b/src/debugProtocol/events/requests/VariablesRequest.ts @@ -0,0 +1,143 @@ +/* eslint-disable no-bitwise */ +import { SmartBuffer } from 'smart-buffer'; +import { Command } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolRequest } from '../ProtocolEvent'; + +export class VariablesRequest implements ProtocolRequest { + + public static fromJson(data: { + requestId: number; + getChildKeys: boolean; + enableForceCaseInsensitivity: boolean; + threadIndex: number; + stackFrameIndex: number; + variablePathEntries: Array<{ + name: string; + forceCaseInsensitive: boolean; + }>; + }) { + const request = new VariablesRequest(); + protocolUtil.loadJson(request, data); + request.data.variablePathEntries ??= []; + // all variables will be case sensitive if the flag is disabled + for (const entry of request.data.variablePathEntries) { + if (request.data.enableForceCaseInsensitivity !== true) { + entry.forceCaseInsensitive = false; + } else { + //default any missing values to false + entry.forceCaseInsensitive ??= false; + } + } + return request; + } + + public static fromBuffer(buffer: Buffer) { + const request = new VariablesRequest(); + protocolUtil.bufferLoaderHelper(request, buffer, 12, (smartBuffer) => { + protocolUtil.loadCommonRequestFields(request, smartBuffer); + + const variableRequestFlags = smartBuffer.readUInt8(); // variable_request_flags + + request.data.getChildKeys = !!(variableRequestFlags & VariableRequestFlag.GetChildKeys); + request.data.enableForceCaseInsensitivity = !!(variableRequestFlags & VariableRequestFlag.CaseSensitivityOptions); + request.data.threadIndex = smartBuffer.readUInt32LE(); // thread_index + request.data.stackFrameIndex = smartBuffer.readUInt32LE(); // stack_frame_index + const variablePathLength = smartBuffer.readUInt32LE(); // variable_path_len + request.data.variablePathEntries = []; + if (variablePathLength > 0) { + for (let i = 0; i < variablePathLength; i++) { + request.data.variablePathEntries.push({ + name: protocolUtil.readStringNT(smartBuffer), // variable_path_entries - optional + //by default, all variable lookups are case SENSITIVE + forceCaseInsensitive: false + }); + } + + //get the case sensitive settings for each part of the path + if (request.data.enableForceCaseInsensitivity) { + for (let i = 0; i < variablePathLength; i++) { + //0 means case SENSITIVE lookup, 1 means forced case INsensitive lookup + request.data.variablePathEntries[i].forceCaseInsensitive = smartBuffer.readUInt8() === 0 ? false : true; + } + } + } + }); + return request; + } + + public toBuffer(): Buffer { + const smartBuffer = new SmartBuffer(); + + //build the flags var + let variableRequestFlags = 0; + variableRequestFlags |= this.data.getChildKeys ? VariableRequestFlag.GetChildKeys : 0; + variableRequestFlags |= this.data.enableForceCaseInsensitivity ? VariableRequestFlag.CaseSensitivityOptions : 0; + + smartBuffer.writeUInt8(variableRequestFlags); // variable_request_flags + smartBuffer.writeUInt32LE(this.data.threadIndex); // thread_index + smartBuffer.writeUInt32LE(this.data.stackFrameIndex); // stack_frame_index + smartBuffer.writeUInt32LE(this.data.variablePathEntries.length); // variable_path_len + for (const entry of this.data.variablePathEntries) { + smartBuffer.writeStringNT(entry.name); // variable_path_entries - optional + } + if (this.data.enableForceCaseInsensitivity) { + for (const entry of this.data.variablePathEntries) { + //0 means case SENSITIVE lookup, 1 means force case INsensitive lookup + smartBuffer.writeUInt8(entry.forceCaseInsensitive !== true ? 0 : 1); + } + } + + protocolUtil.insertCommonRequestFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + /** + * Indicates whether the VARIABLES response includes the child keys for container types like lists and associative arrays. If this is set to true (0x01), the VARIABLES response include the child keys. + */ + getChildKeys: undefined as boolean, + + /** + * Enables the client application to send path_force_case_insensitive data for each variable + */ + enableForceCaseInsensitivity: undefined as boolean, + + /** + * The index of the thread containing the variable. + */ + threadIndex: undefined as number, + /** + * The index of the frame returned from the STACKTRACE command. + * The 0 index contains the first function called; nframes-1 contains the last. + * This indexing does not match the order of the frames returned from the STACKTRACE command + */ + stackFrameIndex: undefined as number, + + /** + * A set of one or more path entries to the variable to be inspected. For example, `m.top.myarray[6]` can be accessed with `["m","top","myarray","6"]`. + * + * If no path is specified, the variables accessible from the specified stack frame are returned. + */ + variablePathEntries: undefined as Array<{ + name: string; + forceCaseInsensitive: boolean; + }>, + + //common props + packetLength: undefined as number, + requestId: undefined as number, + command: Command.Variables + }; +} + +export enum VariableRequestFlag { + GetChildKeys = 1, + CaseSensitivityOptions = 2 +} diff --git a/src/debugProtocol/responses/AddBreakpointsResponse.ts b/src/debugProtocol/events/responses/AddBreakpointsResponse.ts similarity index 100% rename from src/debugProtocol/responses/AddBreakpointsResponse.ts rename to src/debugProtocol/events/responses/AddBreakpointsResponse.ts diff --git a/src/debugProtocol/events/responses/AddConditionalBreakpointsResponse.ts b/src/debugProtocol/events/responses/AddConditionalBreakpointsResponse.ts new file mode 100644 index 00000000..56e9d585 --- /dev/null +++ b/src/debugProtocol/events/responses/AddConditionalBreakpointsResponse.ts @@ -0,0 +1,4 @@ +import { ListBreakpointsResponse } from './ListBreakpointsResponse'; + +//There's currently no difference between this response and the ListBreakpoints response +export class AddConditionalBreakpointsResponse extends ListBreakpointsResponse { } diff --git a/src/debugProtocol/events/responses/ExecuteV3Response.spec.ts b/src/debugProtocol/events/responses/ExecuteV3Response.spec.ts new file mode 100644 index 00000000..467e5ff8 --- /dev/null +++ b/src/debugProtocol/events/responses/ExecuteV3Response.spec.ts @@ -0,0 +1,124 @@ +import { expect } from 'chai'; +import { ErrorCode, StopReasonCode, UpdateType } from '../../Constants'; +import { ExecuteV3Response } from './ExecuteV3Response'; + +describe('ExecuteV3Response', () => { + it('defaults empty arrays for missing error arrays', () => { + const response = ExecuteV3Response.fromJson({} as any); + expect(response.data.compileErrors).to.eql([]); + expect(response.data.runtimeErrors).to.eql([]); + expect(response.data.otherErrors).to.eql([]); + }); + + it('uses default values when data is missing', () => { + let response = ExecuteV3Response.fromJson({} as any); + response.data = {} as any; + response = ExecuteV3Response.fromBuffer( + response.toBuffer() + ); + expect(response.data.executeSuccess).to.eql(false); + expect(response.data.compileErrors).to.eql([]); + expect(response.data.runtimeErrors).to.eql([]); + expect(response.data.otherErrors).to.eql([]); + }); + + it('serializes and deserializes properly', () => { + const command = ExecuteV3Response.fromJson({ + requestId: 3, + executeSuccess: true, + runtimeStopCode: StopReasonCode.Break, + compileErrors: [ + 'compile 1' + ], + runtimeErrors: [ + 'runtime 1' + ], + otherErrors: [ + 'other 1' + ] + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + errorCode: ErrorCode.OK, + + executeSuccess: true, + runtimeStopCode: StopReasonCode.Break, + compileErrors: [ + 'compile 1' + ], + runtimeErrors: [ + 'runtime 1' + ], + otherErrors: [ + 'other 1' + ] + }); + + expect( + ExecuteV3Response.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 54, // 4 bytes + requestId: 3, // 4 bytes + errorCode: ErrorCode.OK, // 4 bytes + + executeSuccess: true, // 1 byte + runtimeStopCode: StopReasonCode.Break, // 1 byte + + // num_compile_errors // 4 bytes + compileErrors: [ + 'compile 1' // 10 bytes + ], + // num_runtime_errors // 4 bytes + runtimeErrors: [ + 'runtime 1' // 10 bytes + ], + // num_other_errors // 4 bytes + otherErrors: [ + 'other 1' // 8 bytes + ] + }); + }); + + it('Handles zero errors', () => { + const command = ExecuteV3Response.fromJson({ + requestId: 3, + executeSuccess: true, + runtimeStopCode: StopReasonCode.Break, + + compileErrors: [], + runtimeErrors: [], + otherErrors: [] + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + errorCode: ErrorCode.OK, + + executeSuccess: true, + runtimeStopCode: StopReasonCode.Break, + compileErrors: [], + runtimeErrors: [], + otherErrors: [] + }); + + expect( + ExecuteV3Response.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 26, // 4 bytes + requestId: 3, // 4 bytes + errorCode: ErrorCode.OK, // 4 bytes + + executeSuccess: true, // 1 byte + runtimeStopCode: StopReasonCode.Break, // 1 byte + // num_compile_errors // 4 bytes + compileErrors: [], + // num_runtime_errors // 4 bytes + runtimeErrors: [], + // num_other_errors // 4 bytes + otherErrors: [] + }); + }); +}); diff --git a/src/debugProtocol/events/responses/ExecuteV3Response.ts b/src/debugProtocol/events/responses/ExecuteV3Response.ts new file mode 100644 index 00000000..e1383278 --- /dev/null +++ b/src/debugProtocol/events/responses/ExecuteV3Response.ts @@ -0,0 +1,115 @@ +import { SmartBuffer } from 'smart-buffer'; +import type { StopReasonCode } from '../../Constants'; +import { ErrorCode } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; + +export class ExecuteV3Response { + public static fromJson(data: { + requestId: number; + executeSuccess: boolean; + runtimeStopCode: StopReasonCode; + compileErrors: string[]; + runtimeErrors: string[]; + otherErrors: string[]; + }) { + const response = new ExecuteV3Response(); + protocolUtil.loadJson(response, data); + response.data.compileErrors ??= []; + response.data.runtimeErrors ??= []; + response.data.otherErrors ??= []; + return response; + } + + public static fromBuffer(buffer: Buffer) { + const response = new ExecuteV3Response(); + protocolUtil.bufferLoaderHelper(response, buffer, 8, (smartBuffer: SmartBuffer) => { + protocolUtil.loadCommonResponseFields(response, smartBuffer); + + response.data.executeSuccess = smartBuffer.readUInt8() !== 0; //execute_success + response.data.runtimeStopCode = smartBuffer.readUInt8(); //runtime_stop_code + + const compileErrorCount = smartBuffer.readUInt32LE(); // num_compile_errors + response.data.compileErrors = []; + for (let i = 0; i < compileErrorCount; i++) { + response.data.compileErrors.push( + protocolUtil.readStringNT(smartBuffer) + ); + } + + const runtimeErrorCount = smartBuffer.readUInt32LE(); // num_runtime_errors + response.data.runtimeErrors = []; + for (let i = 0; i < runtimeErrorCount; i++) { + response.data.runtimeErrors.push( + protocolUtil.readStringNT(smartBuffer) + ); + } + + const otherErrorCount = smartBuffer.readUInt32LE(); // num_other_errors + response.data.otherErrors = []; + for (let i = 0; i < otherErrorCount; i++) { + response.data.otherErrors.push( + protocolUtil.readStringNT(smartBuffer) + ); + } + }); + return response; + } + + public toBuffer() { + const smartBuffer = new SmartBuffer(); + + smartBuffer.writeUInt8(this.data.executeSuccess ? 1 : 0); //execute_success + smartBuffer.writeUInt8(this.data.runtimeStopCode); //runtime_stop_code + + smartBuffer.writeUInt32LE(this.data.compileErrors?.length ?? 0); // num_compile_errors + for (let error of this.data.compileErrors ?? []) { + smartBuffer.writeStringNT(error); + } + + smartBuffer.writeUInt32LE(this.data.runtimeErrors?.length ?? 0); // num_runtime_errors + for (let error of this.data.runtimeErrors ?? []) { + smartBuffer.writeStringNT(error); + } + + smartBuffer.writeUInt32LE(this.data.otherErrors?.length ?? 0); // num_other_errors + for (let error of this.data.otherErrors ?? []) { + smartBuffer.writeStringNT(error); + } + + protocolUtil.insertCommonResponseFields(this, smartBuffer); + + return smartBuffer.toBuffer(); + } + + public success = false; + + public readOffset = 0; + + public data = { + /** + * Indicates whether the code ran and completed without errors (true) + */ + executeSuccess: undefined as boolean, + /** + * A StopReason enum. + */ + runtimeStopCode: undefined as StopReasonCode, + /** + * The list of compile-time errors. + */ + compileErrors: undefined as string[], + /** + * The list of runtime errors. + */ + runtimeErrors: undefined as string[], + /** + * The list of other errors. + */ + otherErrors: undefined as string[], + + //common props + packetLength: undefined as number, + requestId: undefined as number, + errorCode: ErrorCode.OK + }; +} diff --git a/src/debugProtocol/events/responses/GenericResponse.spec.ts b/src/debugProtocol/events/responses/GenericResponse.spec.ts new file mode 100644 index 00000000..78ba8e74 --- /dev/null +++ b/src/debugProtocol/events/responses/GenericResponse.spec.ts @@ -0,0 +1,37 @@ +import { GenericResponse } from './GenericResponse'; +import { expect } from 'chai'; +import { ErrorCode } from '../../Constants'; + +describe('GenericResponse', () => { + it('Handles a Protocol update events', () => { + let response = GenericResponse.fromJson({ + requestId: 3, + errorCode: ErrorCode.CANT_CONTINUE + }); + + expect(response.data).to.eql({ + packetLength: undefined, + errorCode: ErrorCode.CANT_CONTINUE, + requestId: 3 + }); + + response = GenericResponse.fromBuffer(response.toBuffer()); + expect( + response.data + ).to.eql({ + packetLength: 8, // 0 bytes -- this version of the response doesn't have a packet length + errorCode: ErrorCode.CANT_CONTINUE, // 4 bytes + requestId: 3 // 4 bytes + }); + + expect(response.readOffset).to.be.equal(8); + expect(response.success).to.be.equal(true); + }); + + it('Fails when buffer is incomplete', () => { + const buffer = Buffer.alloc(4); + buffer.writeUInt32LE(10); + const response = GenericResponse.fromBuffer(buffer); + expect(response.success).to.equal(false); + }); +}); diff --git a/src/debugProtocol/events/responses/GenericResponse.ts b/src/debugProtocol/events/responses/GenericResponse.ts new file mode 100644 index 00000000..66b59e1e --- /dev/null +++ b/src/debugProtocol/events/responses/GenericResponse.ts @@ -0,0 +1,43 @@ +import { SmartBuffer } from 'smart-buffer'; +import type { ErrorCode } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; + +export class GenericResponse { + public static fromJson(data: { + requestId: number; + errorCode: ErrorCode; + }) { + const response = new GenericResponse(); + protocolUtil.loadJson(response, data); + return response; + } + + public static fromBuffer(buffer: Buffer) { + const response = new GenericResponse(); + protocolUtil.bufferLoaderHelper(response, buffer, 8, (smartBuffer: SmartBuffer) => { + response.data.packetLength = 8; + response.data.requestId = smartBuffer.readUInt32LE(); // request_id + response.data.errorCode = smartBuffer.readUInt32LE(); // error_code + }); + return response; + } + + public toBuffer() { + const smartBuffer = new SmartBuffer(); + smartBuffer.writeUInt32LE(this.data.requestId); // request_id + smartBuffer.writeUInt32LE(this.data.errorCode); // error_code + this.data.packetLength = smartBuffer.writeOffset; + return smartBuffer.toBuffer(); + } + + public success = false; + + public readOffset = 0; + + public data = { + //this response doesn't actually contain packetLength, but we need to add it here just to make this response look like a regular response + packetLength: undefined as number, + requestId: Number.MAX_SAFE_INTEGER, + errorCode: undefined as ErrorCode + }; +} diff --git a/src/debugProtocol/events/responses/GenericV3Response.spec.ts b/src/debugProtocol/events/responses/GenericV3Response.spec.ts new file mode 100644 index 00000000..e88b28d5 --- /dev/null +++ b/src/debugProtocol/events/responses/GenericV3Response.spec.ts @@ -0,0 +1,94 @@ +import { GenericV3Response } from './GenericV3Response'; +import { expect } from 'chai'; +import { ErrorCode } from '../../Constants'; +import { SmartBuffer } from 'smart-buffer'; + +describe('GenericV3Response', () => { + it('serializes and deserializes properly', () => { + const response = GenericV3Response.fromJson({ + errorCode: ErrorCode.OK, + requestId: 3 + }); + + expect(response.data).to.eql({ + packetLength: undefined, + errorCode: ErrorCode.OK, + requestId: 3 + }); + + expect( + GenericV3Response.fromBuffer(response.toBuffer()).data + ).to.eql({ + packetLength: 12, // 4 bytes + errorCode: ErrorCode.OK, // 4 bytes + requestId: 3 // 4 bytes + }); + }); + + it('consumes excess buffer data', () => { + const response = GenericV3Response.fromJson({ + errorCode: ErrorCode.OK, + requestId: 3 + }); + + expect(response.data).to.eql({ + packetLength: undefined, + errorCode: ErrorCode.OK, + requestId: 3 + }); + + const buffer = SmartBuffer.fromBuffer( + //get a buffer without the packetLength + response.toBuffer().slice(4) + ); + while (buffer.writeOffset < 28) { + buffer.writeUInt32LE(1, buffer.length); + } + buffer.insertUInt32LE(buffer.length + 4, 0); //packet_length + + const newResponse = GenericV3Response.fromBuffer(buffer.toBuffer()); + expect(newResponse.readOffset).to.eql(32); + + expect( + newResponse.data + ).to.eql({ + packetLength: 32, // 4 bytes + errorCode: ErrorCode.OK, // 4 bytes + requestId: 3 // 4 bytes + }); + }); + + it('includes error data', () => { + const response = GenericV3Response.fromJson({ + requestId: 3, + errorCode: ErrorCode.INVALID_ARGS, + errorData: { + invalidPathIndex: 1, + missingKeyIndex: 2 + } + }); + + expect(response.data).to.eql({ + packetLength: undefined, + errorCode: ErrorCode.INVALID_ARGS, + requestId: 3, + errorData: { + invalidPathIndex: 1, + missingKeyIndex: 2 + } + }); + + expect( + GenericV3Response.fromBuffer(response.toBuffer()).data + ).to.eql({ + packetLength: 24, // 4 bytes + errorCode: ErrorCode.INVALID_ARGS, // 4 bytes + requestId: 3, // 4 bytes + //error_flags // 4 bytes + errorData: { + invalidPathIndex: 1, // 4 bytes + missingKeyIndex: 2 // 4 bytes + } + }); + }); +}); diff --git a/src/debugProtocol/events/responses/GenericV3Response.ts b/src/debugProtocol/events/responses/GenericV3Response.ts new file mode 100644 index 00000000..86d014f3 --- /dev/null +++ b/src/debugProtocol/events/responses/GenericV3Response.ts @@ -0,0 +1,44 @@ +import { SmartBuffer } from 'smart-buffer'; +import type { ErrorCode } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolResponse, ProtocolResponseData, ProtocolResponseErrorData } from '../ProtocolEvent'; + +export class GenericV3Response implements ProtocolResponse { + public static fromJson(data: { + requestId: number; + errorCode: ErrorCode; + errorData?: ProtocolResponseErrorData; + }) { + const response = new GenericV3Response(); + protocolUtil.loadJson(response, data); + return response; + } + + public static fromBuffer(buffer: Buffer) { + const response = new GenericV3Response(); + protocolUtil.bufferLoaderHelper(response, buffer, 12, (smartBuffer: SmartBuffer) => { + protocolUtil.loadCommonResponseFields(response, smartBuffer); + + //this is a generic response, so we don't actually know what the rest of the payload is. + //so just consume the rest of the payload as throwaway data + response.readOffset = response.data.packetLength; + }); + return response; + } + + public toBuffer() { + const smartBuffer = new SmartBuffer(); + protocolUtil.insertCommonResponseFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + + public readOffset = 0; + + public data = { + packetLength: undefined as number, + requestId: Number.MAX_SAFE_INTEGER, + errorCode: undefined as ErrorCode + } as ProtocolResponseData; +} diff --git a/src/debugProtocol/events/responses/HandshakeResponse.spec.ts b/src/debugProtocol/events/responses/HandshakeResponse.spec.ts new file mode 100644 index 00000000..76f8f2cc --- /dev/null +++ b/src/debugProtocol/events/responses/HandshakeResponse.spec.ts @@ -0,0 +1,72 @@ +import { HandshakeResponse } from './HandshakeResponse'; +import { DebugProtocolClient } from '../../client/DebugProtocolClient'; +import { expect } from 'chai'; +import { HandshakeRequest } from '../requests/HandshakeRequest'; +import { ErrorCode } from '../../Constants'; +import { DEBUGGER_MAGIC } from '../../server/DebugProtocolServer'; + +describe('HandshakeResponse', () => { + it('Handles a handshake response', () => { + const response = HandshakeResponse.fromJson({ + magic: 'not bsdebug', + protocolVersion: '1.0.0' + }); + + expect(response.data).to.eql({ + packetLength: undefined, + requestId: HandshakeRequest.REQUEST_ID, + errorCode: ErrorCode.OK, + + magic: 'not bsdebug', + protocolVersion: '1.0.0' + }); + + expect( + HandshakeResponse.fromBuffer(response.toBuffer()).data + ).to.eql({ + packetLength: undefined, + requestId: HandshakeRequest.REQUEST_ID, + errorCode: ErrorCode.OK, + + magic: 'not bsdebug', // 12 bytes + protocolVersion: '1.0.0' // 12 bytes (each number is sent as uint32) + }); + + expect(response.toBuffer().length).to.eql(24); + }); + + it('uses default version when missing', () => { + const handshake = HandshakeResponse.fromJson({ + magic: DEBUGGER_MAGIC, + protocolVersion: '1.2.3' + }); + handshake.data.protocolVersion = undefined; + expect( + HandshakeResponse.fromBuffer( + handshake.toBuffer() + ).data.protocolVersion + ).to.eql('0.0.0'); + }); + + it('Fails when buffer is incomplete', () => { + let handshake = HandshakeResponse.fromBuffer( + //create a response + HandshakeResponse.fromJson({ + magic: DebugProtocolClient.DEBUGGER_MAGIC, + protocolVersion: '1.0.0' + //slice a few bytes off the end + }).toBuffer().slice(-3) + ); + expect(handshake.success).to.equal(false); + }); + + it('Fails when the protocol version is equal to or greater then 3.0.0', () => { + const response = HandshakeResponse.fromJson({ + magic: 'not bsdebug', + protocolVersion: '3.0.0' + }); + + let handshakeV3 = HandshakeResponse.fromBuffer(response.toBuffer()); + expect(handshakeV3.success).to.equal(false); + }); +}); diff --git a/src/debugProtocol/events/responses/HandshakeResponse.ts b/src/debugProtocol/events/responses/HandshakeResponse.ts new file mode 100644 index 00000000..86e78733 --- /dev/null +++ b/src/debugProtocol/events/responses/HandshakeResponse.ts @@ -0,0 +1,77 @@ +import { SmartBuffer } from 'smart-buffer'; +import * as semver from 'semver'; +import { util } from '../../../util'; +import type { ProtocolEvent, ProtocolResponse } from '../ProtocolEvent'; +import { protocolUtil } from '../../ProtocolUtil'; +import { ErrorCode } from '../../Constants'; +import { HandshakeRequest } from '../requests/HandshakeRequest'; + +export class HandshakeResponse implements ProtocolResponse { + public static fromJson(data: { + magic: string; + protocolVersion: string; + }) { + const response = new HandshakeResponse(); + protocolUtil.loadJson(response, data); + // We only support version prior to v3 with this handshake + if (!semver.satisfies(response.data.protocolVersion, '<3.0.0')) { + response.success = false; + } + return response; + } + + public static fromBuffer(buffer: Buffer) { + const response = new HandshakeResponse(); + protocolUtil.bufferLoaderHelper(response, buffer, 20, (smartBuffer: SmartBuffer) => { + response.data.magic = protocolUtil.readStringNT(smartBuffer); // magic_number + + response.data.protocolVersion = [ + smartBuffer.readInt32LE(), // protocol_major_version + smartBuffer.readInt32LE(), // protocol_minor_version + smartBuffer.readInt32LE() // protocol_patch_version + ].join('.'); + + // We only support version prior to v3 with this handshake + if (!semver.satisfies(response.data.protocolVersion, '<3.0.0')) { + throw new Error(`unsupported version ${response.data.protocolVersion}`); + } + return true; + }); + return response; + } + + public toBuffer() { + let buffer = new SmartBuffer(); + buffer.writeStringNT(this.data.magic); // magic_number + const [major, minor, patch] = (this.data.protocolVersion?.split('.') ?? ['0', '0', '0']).map(x => parseInt(x)); + buffer.writeUInt32LE(major); // protocol_major_version + buffer.writeUInt32LE(minor); // protocol_minor_version + buffer.writeUInt32LE(patch); // protocol_patch_version + + return buffer.toBuffer(); + } + + public success = false; + + public readOffset = 0; + + public data = { + /** + * The Roku Brightscript debug protocol identifier, which is the following 64-bit value :0x0067756265647362LU. + * + * This is equal to 29120988069524322LU or the following little-endian value: b'bsdebug\0. + */ + magic: undefined as string, + /** + * A semantic version string (i.e. `2.0.0`) + */ + protocolVersion: undefined as string, + + + //The handshake response isn't actually structured like like normal responses, but since they're the only unique response, just add dummy data for those fields + packetLength: undefined as number, + //hardcode the max uint32 value. This must be the same value as the HandshakeRequest class + requestId: HandshakeRequest.REQUEST_ID, + errorCode: ErrorCode.OK + }; +} diff --git a/src/debugProtocol/events/responses/HandshakeV3Response.spec.ts b/src/debugProtocol/events/responses/HandshakeV3Response.spec.ts new file mode 100644 index 00000000..e10d7b2c --- /dev/null +++ b/src/debugProtocol/events/responses/HandshakeV3Response.spec.ts @@ -0,0 +1,161 @@ +import { HandshakeV3Response } from './HandshakeV3Response'; +import { DebugProtocolClient } from '../../client/DebugProtocolClient'; +import { expect } from 'chai'; +import { SmartBuffer } from 'smart-buffer'; +import { ErrorCode } from '../../Constants'; +import { HandshakeRequest } from '../requests/HandshakeRequest'; +import { DEBUGGER_MAGIC } from '../../server/DebugProtocolServer'; +import { expectThrows } from '../../../testHelpers.spec'; + +describe('HandshakeV3Response', () => { + const date = new Date(2022, 0, 0); + it('Handles a handshake response', () => { + const response = HandshakeV3Response.fromJson({ + magic: 'bsdebug', + protocolVersion: '3.0.0', + revisionTimestamp: date + }); + + expect(response.data).to.eql({ + packetLength: undefined, + errorCode: ErrorCode.OK, + requestId: HandshakeRequest.REQUEST_ID, + + magic: 'bsdebug', + protocolVersion: '3.0.0', + revisionTimestamp: date + }); + + expect( + HandshakeV3Response.fromBuffer(response.toBuffer()).data + ).to.eql({ + packetLength: undefined, + errorCode: ErrorCode.OK, + requestId: HandshakeRequest.REQUEST_ID, + + magic: 'bsdebug', // 8 bytes + protocolVersion: '3.0.0', // 12 bytes (each number is sent as uint32) + //remaining_packet_length // 4 bytes + revisionTimestamp: date // 8 bytes (int64) + }); + + expect(response.toBuffer().length).to.eql(32); + }); + + it('Handles trailing buffer data in handshake response', () => { + const response = HandshakeV3Response.fromJson({ + magic: 'bsdebug', + protocolVersion: '3.0.0', + revisionTimestamp: date + }); + + //write some extra data to the buffer + const smartBuffer = SmartBuffer.fromBuffer(response.toBuffer()); + smartBuffer.writeStringNT('this is extra data', smartBuffer.length); + + const newResponse = HandshakeV3Response.fromBuffer(smartBuffer.toBuffer()); + expect(newResponse.success).to.be.true; + + expect( + newResponse.data + ).to.eql({ + packetLength: undefined, + errorCode: ErrorCode.OK, + requestId: HandshakeRequest.REQUEST_ID, + magic: 'bsdebug', // 8 bytes + protocolVersion: '3.0.0', // 12 bytes (each number is sent as uint32) + //remaining_packet_length // 4 bytes + revisionTimestamp: date // 8 bytes (int64) + }); + + expect(newResponse.readOffset).to.eql(32); + }); + + it('uses default version when missing', () => { + const handshake = HandshakeV3Response.fromJson({ + magic: DEBUGGER_MAGIC, + protocolVersion: '1.2.3', + revisionTimestamp: new Date() + }); + handshake.data.protocolVersion = undefined; + expect( + HandshakeV3Response.fromBuffer( + handshake.toBuffer() + ).data.protocolVersion + ).to.eql('0.0.0'); + }); + + it('rejects if there was not enough buffer data', () => { + const buffer = new SmartBuffer(); + buffer.writeStringNT(DEBUGGER_MAGIC); + buffer.writeUInt32LE(1); // protocol_major_version + buffer.writeUInt32LE(2); // protocol_minor_version + buffer.writeUInt32LE(3); // protocol_patch_version + buffer.writeUInt32LE(100); //remaining_packet_length. The exception is triggered because this is larger than the remaining buffer size + buffer.writeBigUInt64LE(BigInt(123)); + const response = HandshakeV3Response.fromBuffer(buffer.toBuffer()); + expect(response).to.include({ + readOffset: 0, + success: false + }); + }); + + it('Fails when buffer is incomplete', () => { + let handshake = HandshakeV3Response.fromBuffer( + //create a response + HandshakeV3Response.fromJson({ + magic: DebugProtocolClient.DEBUGGER_MAGIC, + protocolVersion: '1.0.0', + revisionTimestamp: date + //slice a few bytes off the end + }).toBuffer().slice(-3) + ); + expect(handshake.success).to.equal(false); + }); + + it('Fails when the protocol version is less then 3.0.0', () => { + const response = HandshakeV3Response.fromJson({ + magic: 'not bsdebug', + protocolVersion: '2.0.0', + revisionTimestamp: date + }); + + let handshakeV3 = HandshakeV3Response.fromBuffer(response.toBuffer()); + expect(handshakeV3.success).to.equal(false); + }); + + it('parses properly with nonstandard magic', () => { + const response = HandshakeV3Response.fromJson({ + magic: 'not correct magic', + protocolVersion: '3.1.0', + revisionTimestamp: date + }); + + expect(response.data).to.eql({ + packetLength: undefined, + errorCode: ErrorCode.OK, + requestId: HandshakeRequest.REQUEST_ID, + + magic: 'not correct magic', + protocolVersion: '3.1.0', + revisionTimestamp: date + }); + + expect( + HandshakeV3Response.fromBuffer(response.toBuffer()).data + ).to.eql({ + packetLength: undefined, + errorCode: ErrorCode.OK, + requestId: HandshakeRequest.REQUEST_ID, + + magic: 'not correct magic', // 18 bytes + protocolVersion: '3.1.0', // 12 bytes (each number is sent as uint32) + //remaining_packet_length // 4 bytes + revisionTimestamp: date // 8 bytes (int64) + }); + + expect(response.toBuffer().length).to.eql(42); + const parsed = HandshakeV3Response.fromBuffer(response.toBuffer()); + expect(parsed.readOffset).to.eql(42); + }); +}); diff --git a/src/debugProtocol/events/responses/HandshakeV3Response.ts b/src/debugProtocol/events/responses/HandshakeV3Response.ts new file mode 100644 index 00000000..01eca514 --- /dev/null +++ b/src/debugProtocol/events/responses/HandshakeV3Response.ts @@ -0,0 +1,114 @@ +import { SmartBuffer } from 'smart-buffer'; +import * as semver from 'semver'; +import type { ProtocolResponse } from '../ProtocolEvent'; +import { protocolUtil } from '../../ProtocolUtil'; +import { ErrorCode } from '../../Constants'; +import { HandshakeRequest } from '../requests/HandshakeRequest'; + +export class HandshakeV3Response implements ProtocolResponse { + + public static fromJson(data: { + magic: string; + protocolVersion: string; + revisionTimestamp: Date; + }) { + const response = new HandshakeV3Response(); + protocolUtil.loadJson(response, data); + // We only support v3 or above with this handshake + if (semver.satisfies(response.data.protocolVersion, '<3.0.0')) { + response.success = false; + } + return response; + } + + public static fromBuffer(buffer: Buffer) { + const response = new HandshakeV3Response(); + protocolUtil.bufferLoaderHelper(response, buffer, 20, (smartBuffer: SmartBuffer) => { + response.data.magic = protocolUtil.readStringNT(smartBuffer); // magic_number + + response.data.protocolVersion = [ + smartBuffer.readUInt32LE(), // protocol_major_version + smartBuffer.readUInt32LE(), // protocol_minor_version + smartBuffer.readUInt32LE() // protocol_patch_version + ].join('.'); + + const legacyReadSize = smartBuffer.readOffset; + const remainingPacketLength = smartBuffer.readInt32LE(); // remaining_packet_length + + const requiredBufferSize = remainingPacketLength + legacyReadSize; + response.data.revisionTimestamp = new Date(Number(smartBuffer.readBigUInt64LE())); // platform_revision_timestamp + + if (smartBuffer.length < requiredBufferSize) { + throw new Error(`Missing buffer data according to the remaining packet length: ${smartBuffer.length}/${requiredBufferSize}`); + } + //set the buffer offset + smartBuffer.readOffset = requiredBufferSize; + + // We only support v3.0.0 or above with this handshake + if (semver.satisfies(response.data.protocolVersion, '<3.0.0')) { + throw new Error(`unsupported version ${response.data.protocolVersion}`); + } + }); + return response; + } + + /** + * Convert the data into a buffer + */ + public toBuffer() { + let smartBuffer = new SmartBuffer(); + smartBuffer.writeStringNT(this.data.magic); // magic_number + const [major, minor, patch] = (this.data.protocolVersion?.split('.') ?? ['0', '0', '0']).map(x => parseInt(x)); + smartBuffer.writeUInt32LE(major); // protocol_major_version + smartBuffer.writeUInt32LE(minor); // protocol_minor_version + smartBuffer.writeUInt32LE(patch); // protocol_patch_version + + //As of BrightScript debug protocol 3.0.0 (Roku OS 11.0), all packets from the debugging target include a packet_length. + //The length is always in bytes, and includes the packet_length field, itself. + //This field avoids the need for changes to the major version of the protocol because it allows a debugger client to + //read past data it does not understand and is not critical to debugger operations. + const remainingDataBuffer = new SmartBuffer(); + remainingDataBuffer.writeBigInt64LE(BigInt( + this.data.revisionTimestamp.getTime() + )); // platform_revision_timestamp + + smartBuffer.writeUInt32LE(remainingDataBuffer.writeOffset + 4); // remaining_packet_length + smartBuffer.writeBuffer(remainingDataBuffer.toBuffer()); + + return smartBuffer.toBuffer(); + } + + public success = false; + + public readOffset = 0; + + public data = { + /** + * The Roku Brightscript debug protocol identifier, which is the following 64-bit value :0x0067756265647362LU. + * + * This is equal to 29120988069524322LU or the following little-endian value: b'bsdebug\0. + */ + magic: undefined as string, + /** + * A semantic version string (i.e. `2.0.0`) + */ + protocolVersion: undefined as string, + /** + * A platform-specific implementation timestamp (in milliseconds since epoch [1970-01-01T00:00:00.000Z]). + * + * As of BrightScript debug protocol 3.0.0 (Roku OS 11.0), a timestamp is sent to the debugger client in the initial handshake. + * This timestamp is platform-specific data that is included in the system software of the platform being debugged. + * It is changed by the platform's vendor when there is any change that affects the behavior of the debugger. + * + * The value can be used in manners similar to a build number, and is primarily used to differentiate between pre-release builds of the platform being debugged. + */ + revisionTimestamp: undefined as Date, + + + //The handshake response isn't actually structured like like normal responses, but since they're the only unique response, just add dummy data for those fields + packetLength: undefined as number, + //hardcode the max uint32 integer value. This must be the same value as the HandshakeRequest class + requestId: HandshakeRequest.REQUEST_ID, + errorCode: ErrorCode.OK + }; +} diff --git a/src/debugProtocol/events/responses/ListBreakpointsResponse.spec.ts b/src/debugProtocol/events/responses/ListBreakpointsResponse.spec.ts new file mode 100644 index 00000000..c43dee10 --- /dev/null +++ b/src/debugProtocol/events/responses/ListBreakpointsResponse.spec.ts @@ -0,0 +1,193 @@ +import { expect } from 'chai'; +import { ListBreakpointsResponse } from './ListBreakpointsResponse'; +import { ErrorCode } from '../../Constants'; +import { getRandomBuffer } from '../../../testHelpers.spec'; + +describe('ListBreakpointsResponse', () => { + it('defaults undefined breakpoint array to empty', () => { + let response = ListBreakpointsResponse.fromJson({} as any); + expect(response.data.breakpoints).to.eql([]); + }); + + it('skips ignoreCount for invalid breakpoints', () => { + const response = ListBreakpointsResponse.fromBuffer( + ListBreakpointsResponse.fromJson({ + requestId: 2, + breakpoints: [{ + id: 12, + errorCode: ErrorCode.OK, + ignoreCount: 10 + }, { + id: 0, + errorCode: ErrorCode.OK, + ignoreCount: 20 + }] + }).toBuffer() + ); + expect(response.data.breakpoints[0].ignoreCount).to.eql(10); + expect(response.data.breakpoints[1].ignoreCount).to.eql(undefined); + + }); + + it('defaults num_breakpoints to 0 if array is missing', () => { + let response = ListBreakpointsResponse.fromJson({} as any); + response.data = {} as any; + response = ListBreakpointsResponse.fromBuffer( + response.toBuffer() + ); + expect(response.data.breakpoints).to.eql([]); + }); + + it('serializes and deserializes multiple breakpoints properly', () => { + let response = ListBreakpointsResponse.fromJson({ + requestId: 3, + breakpoints: [{ + errorCode: ErrorCode.OK, + id: 10, + ignoreCount: 2 + }, { + errorCode: ErrorCode.OK, + id: 20, + ignoreCount: 3 + }] + }); + + expect(response.data).to.eql({ + packetLength: undefined, + requestId: 3, + errorCode: ErrorCode.OK, + breakpoints: [{ + errorCode: ErrorCode.OK, + id: 10, + ignoreCount: 2 + }, { + errorCode: ErrorCode.OK, + id: 20, + ignoreCount: 3 + }] + }); + + response = ListBreakpointsResponse.fromBuffer(response.toBuffer()); + + expect( + response.data + ).to.eql({ + packetLength: 40, // 4 bytes + requestId: 3, // 4 bytes, + errorCode: ErrorCode.OK, // 4 bytes + //num_breakpoints // 4 bytes + breakpoints: [{ + errorCode: ErrorCode.OK, // 4 bytes + id: 10, // 4 bytes + ignoreCount: 2 // 4 bytes + }, { + errorCode: ErrorCode.OK, // 4 bytes + id: 20, // 4 bytes + ignoreCount: 3 // 4 bytes + }] + }); + }); + + it('handles empty breakpoints array', () => { + let response = ListBreakpointsResponse.fromJson({ + requestId: 3, + breakpoints: [] + }); + + expect(response.data).to.eql({ + packetLength: undefined, + requestId: 3, + errorCode: ErrorCode.OK, + breakpoints: [] + }); + + response = ListBreakpointsResponse.fromBuffer(response.toBuffer()); + + expect( + response.data + ).to.eql({ + packetLength: 16, // 4 bytes + requestId: 3, // 4 bytes, + errorCode: ErrorCode.OK, // 4 bytes + //num_breakpoints // 4 bytes + breakpoints: [] + }); + }); + + it('handles empty buffer', () => { + const response = ListBreakpointsResponse.fromBuffer(null); + //Great, it didn't explode! + expect(response.success).to.be.false; + }); + + it('handles undersized buffers', () => { + let response = ListBreakpointsResponse.fromBuffer( + getRandomBuffer(0) + ); + expect(response.success).to.be.false; + + response = ListBreakpointsResponse.fromBuffer( + getRandomBuffer(1) + ); + expect(response.success).to.be.false; + + response = ListBreakpointsResponse.fromBuffer( + getRandomBuffer(11) + ); + expect(response.success).to.be.false; + }); + + it('gracefully handles mismatched breakpoint count', () => { + let buffer = ListBreakpointsResponse.fromJson({ + requestId: 3, + breakpoints: [{ + errorCode: ErrorCode.OK, + id: 1, + ignoreCount: 0 + }] + }).toBuffer(); + + //set num_breakpoints to 2 instead of 1 + buffer = Buffer.concat([ + buffer.slice(0, 12), + Buffer.from([2, 0, 0, 0]), + buffer.slice(16) + ]); + + const response = ListBreakpointsResponse.fromBuffer(buffer); + expect(response.success).to.be.false; + expect(response.data.breakpoints).to.eql([{ + errorCode: ErrorCode.OK, + id: 1, + ignoreCount: 0 + }]); + }); + + it('handles malformed breakpoint data', () => { + let buffer = ListBreakpointsResponse.fromJson({ + requestId: 3, + breakpoints: [{ + errorCode: ErrorCode.OK, + id: 1, + ignoreCount: 0 + }, { + errorCode: ErrorCode.OK, + id: 2, + ignoreCount: 0 + }] + }).toBuffer(); + + //set num_breakpoints to 2 instead of 1 + buffer = Buffer.concat([ + buffer.slice(0, buffer.length - 3) + ]); + + const response = ListBreakpointsResponse.fromBuffer(buffer); + expect(response.success).to.be.false; + expect(response.data.breakpoints).to.eql([{ + errorCode: ErrorCode.OK, + id: 1, + ignoreCount: 0 + }]); + }); +}); diff --git a/src/debugProtocol/events/responses/ListBreakpointsResponse.ts b/src/debugProtocol/events/responses/ListBreakpointsResponse.ts new file mode 100644 index 00000000..e093e4cc --- /dev/null +++ b/src/debugProtocol/events/responses/ListBreakpointsResponse.ts @@ -0,0 +1,90 @@ +import { SmartBuffer } from 'smart-buffer'; +import { ErrorCode } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolResponse } from '../ProtocolEvent'; + +export class ListBreakpointsResponse implements ProtocolResponse { + + public static fromJson(data: { + requestId: number; + breakpoints: BreakpointInfo[]; + }) { + const response = new ListBreakpointsResponse(); + protocolUtil.loadJson(response, data); + response.data.breakpoints ??= []; + return response; + } + + public static fromBuffer(buffer: Buffer) { + const response = new ListBreakpointsResponse(); + protocolUtil.bufferLoaderHelper(response, buffer, 12, (smartBuffer: SmartBuffer) => { + protocolUtil.loadCommonResponseFields(response, smartBuffer); + const numBreakpoints = smartBuffer.readUInt32LE(); // num_breakpoints + + response.data.breakpoints = []; + + // build the list of BreakpointInfo + for (let i = 0; i < numBreakpoints; i++) { + const breakpoint = {} as BreakpointInfo; + // breakpoint_id - The ID assigned to the breakpoint. An ID greater than 0 indicates an active breakpoint. An ID of 0 denotes that the breakpoint has an error. + breakpoint.id = smartBuffer.readUInt32LE(); + // error_code - Indicates whether the breakpoint was successfully returned. + breakpoint.errorCode = smartBuffer.readUInt32LE(); + + if (breakpoint.id > 0) { + // This value is only present if the breakpoint_id is valid. + // ignore_count - Current state, decreases as breakpoint is executed. + breakpoint.ignoreCount = smartBuffer.readUInt32LE(); + } + response.data.breakpoints.push(breakpoint); + } + return response.data.breakpoints.length === numBreakpoints; + }); + return response; + } + + public toBuffer() { + const smartBuffer = new SmartBuffer(); + smartBuffer.writeUInt32LE(this.data.breakpoints?.length ?? 0); // num_breakpoints + for (const breakpoint of this.data.breakpoints ?? []) { + smartBuffer.writeUInt32LE(breakpoint.id); // breakpoint_id + smartBuffer.writeUInt32LE(breakpoint.errorCode); // error_code + //if this breakpoint has no errors, then write its ignore_count + if (breakpoint.id > 0) { + smartBuffer.writeUInt32LE(breakpoint.ignoreCount); // ignore_count + } + } + protocolUtil.insertCommonResponseFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + + public readOffset = 0; + + public data = { + breakpoints: [] as BreakpointInfo[], + + // response fields + packetLength: undefined as number, + requestId: undefined as number, + errorCode: ErrorCode.OK + }; +} + +export interface BreakpointInfo { + /** + * The ID assigned to the breakpoint. An ID greater than 0 indicates an active breakpoint. An ID of 0 denotes that the breakpoint has an error. + */ + id: number; + /** + * Indicates whether the breakpoint was successfully returned. This may be one of the following values: + * - `0` (`'OK'`) - The breakpoint_id is valid. + * - `5` (`'INVALID_ARGS'`) - The breakpoint could not be returned. + */ + errorCode: number; + /** + * Current state, decreases as breakpoint is executed. This argument is only present if the breakpoint_id is valid. + */ + ignoreCount: number; +} diff --git a/src/debugProtocol/responses/RemoveBreakpointsResponse.ts b/src/debugProtocol/events/responses/RemoveBreakpointsResponse.ts similarity index 100% rename from src/debugProtocol/responses/RemoveBreakpointsResponse.ts rename to src/debugProtocol/events/responses/RemoveBreakpointsResponse.ts diff --git a/src/debugProtocol/events/responses/StackTraceResponse.spec.ts b/src/debugProtocol/events/responses/StackTraceResponse.spec.ts new file mode 100644 index 00000000..90564bd3 --- /dev/null +++ b/src/debugProtocol/events/responses/StackTraceResponse.spec.ts @@ -0,0 +1,120 @@ +import { expect } from 'chai'; +import { StackTraceResponse } from './StackTraceResponse'; +import { ErrorCode } from '../../Constants'; +import { getRandomBuffer } from '../../../testHelpers.spec'; + +describe('StackTraceResponse', () => { + it('defaults data.entries to empty array when missing', () => { + let response = StackTraceResponse.fromJson({} as any); + expect(response.data.entries).to.eql([]); + }); + + it('does not crash when data is invalid', () => { + let response = StackTraceResponse.fromJson({} as any); + response.data = {} as any; + response = StackTraceResponse.fromBuffer(response.toBuffer()); + expect(response.data.entries).to.eql([]); + }); + + it('serializes and deserializes multiple breakpoints properly', () => { + let response = StackTraceResponse.fromJson({ + requestId: 3, + entries: [{ + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs' + }, { + lineNumber: 1, + functionName: 'libFunc', + filePath: 'pkg:/source/lib.brs' + }] + }); + + expect(response.data).to.eql({ + packetLength: undefined, + requestId: 3, + errorCode: ErrorCode.OK, + entries: [{ + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs' + }, { + lineNumber: 1, + functionName: 'libFunc', + filePath: 'pkg:/source/lib.brs' + }] + }); + + response = StackTraceResponse.fromBuffer(response.toBuffer()); + + expect( + response.data + ).to.eql({ + packetLength: undefined, // 0 bytes + requestId: 3, // 4 bytes, + errorCode: ErrorCode.OK, // 4 bytes + // num_entries // 4 bytes + entries: [{ + lineNumber: 2, // 4 bytes + functionName: 'main', // 5 bytes + filePath: 'pkg:/source/main.brs' // 21 bytes + }, { + lineNumber: 1, // 4 bytes + functionName: 'libFunc', // 8 bytes + filePath: 'pkg:/source/lib.brs' // 20 bytes + }] + }); + + expect(response.readOffset).to.eql(74); + }); + + it('handles empty entries array', () => { + let response = StackTraceResponse.fromJson({ + requestId: 3, + entries: [] + }); + + expect(response.data).to.eql({ + packetLength: undefined, + requestId: 3, + errorCode: ErrorCode.OK, + entries: [] + }); + + response = StackTraceResponse.fromBuffer(response.toBuffer()); + + expect( + response.data + ).to.eql({ + packetLength: undefined, // 0 bytes + requestId: 3, // 4 bytes, + errorCode: ErrorCode.OK, // 4 bytes + // num_entries // 4 bytes + entries: [] + }); + expect(response.readOffset).to.eql(12); + }); + + it('handles empty buffer', () => { + const response = StackTraceResponse.fromBuffer(null); + //Great, it didn't explode! + expect(response.success).to.be.false; + }); + + it('handles undersized buffers', () => { + let response = StackTraceResponse.fromBuffer( + getRandomBuffer(0) + ); + expect(response.success).to.be.false; + + response = StackTraceResponse.fromBuffer( + getRandomBuffer(1) + ); + expect(response.success).to.be.false; + + response = StackTraceResponse.fromBuffer( + getRandomBuffer(11) + ); + expect(response.success).to.be.false; + }); +}); diff --git a/src/debugProtocol/events/responses/StackTraceResponse.ts b/src/debugProtocol/events/responses/StackTraceResponse.ts new file mode 100644 index 00000000..6a705b6d --- /dev/null +++ b/src/debugProtocol/events/responses/StackTraceResponse.ts @@ -0,0 +1,81 @@ +import { SmartBuffer } from 'smart-buffer'; +import { ErrorCode } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { StackEntry } from './StackTraceV3Response'; + +export class StackTraceResponse { + + public static fromJson(data: { + requestId: number; + entries: StackEntry[]; + }) { + const response = new StackTraceResponse(); + protocolUtil.loadJson(response, data); + response.data.entries ??= []; + return response; + } + + public static fromBuffer(buffer: Buffer) { + const response = new StackTraceResponse(); + protocolUtil.bufferLoaderHelper(response, buffer, 12, (smartBuffer: SmartBuffer) => { + response.data.requestId = smartBuffer.readUInt32LE(); // request_id + response.data.errorCode = smartBuffer.readUInt32LE(); // error_code + + const stackSize = smartBuffer.readUInt32LE(); // stack_size + + response.data.entries = []; + + // build the list of BreakpointInfo + for (let i = 0; i < stackSize; i++) { + const entry = {} as StackEntry; + entry.lineNumber = smartBuffer.readUInt32LE(); + // NOTE: this is documented as being function name then file name but it is being returned by the device backwards. + entry.filePath = protocolUtil.readStringNT(smartBuffer); + entry.functionName = protocolUtil.readStringNT(smartBuffer); + + // TODO do we need this anymore? + // let fileExtension = path.extname(this.fileName).toLowerCase(); + // // NOTE:Make sure we have a full valid path (?? can be valid because the device might not know the file). + // entry.success = (fileExtension === '.brs' || fileExtension === '.xml' || this.fileName === '??'); + response.data.entries.push(entry); + } + }); + return response; + } + + public toBuffer() { + const smartBuffer = new SmartBuffer(); + smartBuffer.writeUInt32LE(this.data.requestId); // request_id + smartBuffer.writeUInt32LE(this.data.errorCode); // error_code + + smartBuffer.writeUInt32LE(this.data.entries?.length ?? 0); // stack_size + for (const entry of this.data.entries ?? []) { + smartBuffer.writeUInt32LE(entry.lineNumber); // line_number + // NOTE: this is documented as being function name then file name but it is being returned by the device backwards. + smartBuffer.writeStringNT(entry.filePath); // file_path + smartBuffer.writeStringNT(entry.functionName); // function_name + } + + this.data.packetLength = smartBuffer.writeOffset; + return smartBuffer.toBuffer(); + } + + public success = false; + + public readOffset = 0; + + public data = { + /** + * An array of StrackEntry structs. entries[0] contains the last function called; + * entries[stack_size-1] contains the first function called. + * Debugging clients may reverse the entries to match developer expectations. + */ + entries: undefined as StackEntry[], + + // response fields + packetLength: undefined as number, + requestId: undefined as number, + errorCode: ErrorCode.OK + }; + +} diff --git a/src/debugProtocol/events/responses/StackTraceV3Response.spec.ts b/src/debugProtocol/events/responses/StackTraceV3Response.spec.ts new file mode 100644 index 00000000..95abddcf --- /dev/null +++ b/src/debugProtocol/events/responses/StackTraceV3Response.spec.ts @@ -0,0 +1,171 @@ +import { expect } from 'chai'; +import { StackTraceV3Response } from './StackTraceV3Response'; +import { ErrorCode } from '../../Constants'; +import { getRandomBuffer } from '../../../testHelpers.spec'; + +describe('StackTraceV3Response', () => { + it('defaults data.entries to empty array when missing', () => { + let response = StackTraceV3Response.fromJson({} as any); + expect(response.data.entries).to.eql([]); + }); + + it('does not crash when data is invalid', () => { + let response = StackTraceV3Response.fromJson({} as any); + response.data = {} as any; + response = StackTraceV3Response.fromBuffer(response.toBuffer()); + expect(response.data.entries).to.eql([]); + }); + + it('serializes and deserializes multiple breakpoints properly', () => { + let response = StackTraceV3Response.fromJson({ + requestId: 3, + entries: [{ + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs' + }, { + lineNumber: 1, + functionName: 'libFunc', + filePath: 'pkg:/source/lib.brs' + }] + }); + + expect(response.data).to.eql({ + packetLength: undefined, + requestId: 3, + errorCode: ErrorCode.OK, + entries: [{ + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs' + }, { + lineNumber: 1, + functionName: 'libFunc', + filePath: 'pkg:/source/lib.brs' + }] + }); + + response = StackTraceV3Response.fromBuffer(response.toBuffer()); + + expect( + response.data + ).to.eql({ + packetLength: 78, // 4 bytes + requestId: 3, // 4 bytes, + errorCode: ErrorCode.OK, // 4 bytes + // num_entries // 4 bytes + entries: [{ + lineNumber: 2, // 4 bytes + functionName: 'main', // 5 bytes + filePath: 'pkg:/source/main.brs' // 21 bytes + }, { + lineNumber: 1, // 4 bytes + functionName: 'libFunc', // 8 bytes + filePath: 'pkg:/source/lib.brs' // 20 bytes + }] + }); + }); + + it('handles empty entries array', () => { + let response = StackTraceV3Response.fromJson({ + requestId: 3, + entries: [] + }); + + expect(response.data).to.eql({ + packetLength: undefined, + requestId: 3, + errorCode: ErrorCode.OK, + entries: [] + }); + + response = StackTraceV3Response.fromBuffer(response.toBuffer()); + + expect( + response.data + ).to.eql({ + packetLength: 16, // 4 bytes + requestId: 3, // 4 bytes, + errorCode: ErrorCode.OK, // 4 bytes + // num_entries // 4 bytes + entries: [] + }); + }); + + it('handles empty buffer', () => { + const response = StackTraceV3Response.fromBuffer(null); + //Great, it didn't explode! + expect(response.success).to.be.false; + }); + + it('handles undersized buffers', () => { + let response = StackTraceV3Response.fromBuffer( + getRandomBuffer(0) + ); + expect(response.success).to.be.false; + + response = StackTraceV3Response.fromBuffer( + getRandomBuffer(1) + ); + expect(response.success).to.be.false; + + response = StackTraceV3Response.fromBuffer( + getRandomBuffer(11) + ); + expect(response.success).to.be.false; + }); + + it('gracefully handles mismatched breakpoint count', () => { + let buffer = StackTraceV3Response.fromJson({ + requestId: 3, + entries: [{ + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs' + }] + }).toBuffer(); + + //set num_breakpoints to 2 instead of 1 + buffer = Buffer.concat([ + buffer.slice(0, 12), + Buffer.from([2, 0, 0, 0]), + buffer.slice(16) + ]); + + const response = StackTraceV3Response.fromBuffer(buffer); + expect(response.success).to.be.false; + expect(response.data.entries).to.eql([{ + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs' + }]); + }); + + it('handles malformed breakpoint data', () => { + let buffer = StackTraceV3Response.fromJson({ + requestId: 3, + entries: [{ + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs' + }, { + lineNumber: 1, + functionName: 'libFunc', + filePath: 'pkg:/source/lib.brs' + }] + }).toBuffer(); + + // remove some trailing data + buffer = Buffer.concat([ + buffer.slice(0, buffer.length - 3) + ]); + + const response = StackTraceV3Response.fromBuffer(buffer); + expect(response.success).to.be.false; + expect(response.data.entries).to.eql([{ + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs' + }]); + }); +}); diff --git a/src/debugProtocol/events/responses/StackTraceV3Response.ts b/src/debugProtocol/events/responses/StackTraceV3Response.ts new file mode 100644 index 00000000..7a6bf7a7 --- /dev/null +++ b/src/debugProtocol/events/responses/StackTraceV3Response.ts @@ -0,0 +1,88 @@ +import { SmartBuffer } from 'smart-buffer'; +import { ErrorCode } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; + +export class StackTraceV3Response { + + public static fromJson(data: { + requestId: number; + entries: StackEntry[]; + }) { + const response = new StackTraceV3Response(); + protocolUtil.loadJson(response, data); + response.data.entries ??= []; + return response; + } + + public static fromBuffer(buffer: Buffer) { + const response = new StackTraceV3Response(); + protocolUtil.bufferLoaderHelper(response, buffer, 16, (smartBuffer: SmartBuffer) => { + protocolUtil.loadCommonResponseFields(response, smartBuffer); + + const stackSize = smartBuffer.readUInt32LE(); // stack_size + + response.data.entries = []; + + // build the list of BreakpointInfo + for (let i = 0; i < stackSize; i++) { + const entry = {} as StackEntry; + entry.lineNumber = smartBuffer.readUInt32LE(); + entry.functionName = protocolUtil.readStringNT(smartBuffer); + entry.filePath = protocolUtil.readStringNT(smartBuffer); + + // TODO do we need this anymore? + // let fileExtension = path.extname(this.fileName).toLowerCase(); + // // NOTE:Make sure we have a full valid path (?? can be valid because the device might not know the file). + // entry.success = (fileExtension === '.brs' || fileExtension === '.xml' || this.fileName === '??'); + response.data.entries.push(entry); + } + }); + return response; + } + + public toBuffer() { + const smartBuffer = new SmartBuffer(); + smartBuffer.writeUInt32LE(this.data.entries?.length ?? 0); // stack_size + for (const entry of this.data.entries ?? []) { + smartBuffer.writeUInt32LE(entry.lineNumber); // line_number + smartBuffer.writeStringNT(entry.functionName); // function_name + smartBuffer.writeStringNT(entry.filePath); // file_path + } + protocolUtil.insertCommonResponseFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + + public readOffset = 0; + + public data = { + /** + * An array of StrackEntry structs. entries[0] contains the last function called; + * entries[stack_size-1] contains the first function called. + * Debugging clients may reverse the entries to match developer expectations. + */ + entries: undefined as StackEntry[], + + // response fields + packetLength: undefined as number, + requestId: undefined as number, + errorCode: ErrorCode.OK + }; + +} + +export interface StackEntry { + /** + * The line number where the stop or failure occurred. + */ + lineNumber: number; + /** + * The function where the stop or failure occurred. + */ + functionName: string; + /** + * The file where the stop or failure occurred. + */ + filePath: string; +} diff --git a/src/debugProtocol/events/responses/ThreadsResponse.spec.ts b/src/debugProtocol/events/responses/ThreadsResponse.spec.ts new file mode 100644 index 00000000..a16f9049 --- /dev/null +++ b/src/debugProtocol/events/responses/ThreadsResponse.spec.ts @@ -0,0 +1,221 @@ +import { expect } from 'chai'; +import { ThreadsResponse } from './ThreadsResponse'; +import { ErrorCode, StopReason } from '../../Constants'; +import { getRandomBuffer } from '../../../testHelpers.spec'; +import { StackTraceV3Response } from './StackTraceV3Response'; + +describe('ThreadsResponse', () => { + it('defaults data.entries to empty array when missing', () => { + let response = ThreadsResponse.fromJson({} as any); + expect(response.data.threads).to.eql([]); + }); + + it('does not crash when data is invalid', () => { + let response = ThreadsResponse.fromJson({} as any); + response.data = {} as any; + response = ThreadsResponse.fromBuffer(response.toBuffer()); + expect(response.data.threads).to.eql([]); + }); + + function t(extra?: Record) { + return { + isPrimary: true, + stopReason: StopReason.Break, + stopReasonDetail: 'because', + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs', + codeSnippet: 'sub main()', + ...extra ?? {} + }; + } + + it('defaults unspecified threads as not primary', () => { + const response = ThreadsResponse.fromBuffer( + ThreadsResponse.fromJson({ + threads: [t({ + isPrimary: undefined + }), t({ + isPrimary: true + }), t({ + isPrimary: false + })] + } as any).toBuffer() + ); + expect(response.data.threads.map(x => x.isPrimary)).to.eql([false, true, false]); + }); + + it('serializes and deserializes multiple breakpoints properly', () => { + let response = ThreadsResponse.fromJson({ + requestId: 3, + threads: [{ + isPrimary: true, + stopReason: StopReason.Break, + stopReasonDetail: 'because', + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs', + codeSnippet: 'sub main()' + }] + }); + + expect(response.data).to.eql({ + packetLength: undefined, + requestId: 3, + errorCode: ErrorCode.OK, + threads: [{ + isPrimary: true, + stopReason: 'Break', + stopReasonDetail: 'because', + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs', + codeSnippet: 'sub main()' + }] + }); + + response = ThreadsResponse.fromBuffer(response.toBuffer()); + + expect( + response.data + ).to.eql({ + packetLength: 70, // 4 bytes + requestId: 3, // 4 bytes, + errorCode: ErrorCode.OK, // 4 bytes + // threads_count // 4 bytes + threads: [{ + // flags // 4 bytes + isPrimary: true, // 0 bytes - part of flags + stopReason: 'Break', // 1 byte + stopReasonDetail: 'because', // 8 bytes + lineNumber: 2, // 4 bytes + functionName: 'main', // 5 bytes + filePath: 'pkg:/source/main.brs', // 21 bytes + codeSnippet: 'sub main()' // 11 bytes + }] + }); + }); + + it('handles empty entries array', () => { + let response = ThreadsResponse.fromJson({ + requestId: 3, + threads: [] + }); + + expect(response.data).to.eql({ + packetLength: undefined, + requestId: 3, + errorCode: ErrorCode.OK, + threads: [] + }); + + response = ThreadsResponse.fromBuffer(response.toBuffer()); + + expect( + response.data + ).to.eql({ + packetLength: 16, // 4 bytes + requestId: 3, // 4 bytes, + errorCode: ErrorCode.OK, // 4 bytes + // threads_count // 4 bytes + threads: [] + }); + }); + + it('handles empty buffer', () => { + const response = ThreadsResponse.fromBuffer(null); + //Great, it didn't explode! + expect(response.success).to.be.false; + }); + + it('handles undersized buffers', () => { + let response = ThreadsResponse.fromBuffer( + getRandomBuffer(0) + ); + expect(response.success).to.be.false; + + response = ThreadsResponse.fromBuffer( + getRandomBuffer(1) + ); + expect(response.success).to.be.false; + + response = ThreadsResponse.fromBuffer( + getRandomBuffer(11) + ); + expect(response.success).to.be.false; + }); + + it('gracefully handles mismatched breakpoint count', () => { + let buffer = ThreadsResponse.fromJson({ + requestId: 3, + threads: [{ + isPrimary: true, + stopReason: StopReason.Break, + stopReasonDetail: 'because', + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs', + codeSnippet: 'sub main()' + }] + }).toBuffer(); + + //set num_breakpoints to 2 instead of 1 + buffer = Buffer.concat([ + buffer.slice(0, 12), + Buffer.from([2, 0, 0, 0]), + buffer.slice(16) + ]); + + const response = ThreadsResponse.fromBuffer(buffer); + expect(response.success).to.be.false; + expect(response.data.threads).to.eql([{ + isPrimary: true, + stopReason: 'Break', + stopReasonDetail: 'because', + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs', + codeSnippet: 'sub main()' + }]); + }); + + it('handles malformed breakpoint data', () => { + let buffer = ThreadsResponse.fromJson({ + requestId: 3, + threads: [{ + isPrimary: true, + stopReason: StopReason.Break, + stopReasonDetail: 'because', + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs', + codeSnippet: 'sub main()' + }, { + isPrimary: true, + stopReason: StopReason.StopStatement, + stopReasonDetail: 'because', + lineNumber: 3, + functionName: 'main', + filePath: 'pkg:/source/main.brs', + codeSnippet: 'sub main()' + }] + }).toBuffer(); + + // remove some trailing data + buffer = Buffer.concat([ + buffer.slice(0, buffer.length - 3) + ]); + + const response = ThreadsResponse.fromBuffer(buffer); + expect(response.success).to.be.false; + expect(response.data.threads).to.eql([{ + isPrimary: true, + stopReason: StopReason.Break, + stopReasonDetail: 'because', + lineNumber: 2, + functionName: 'main', + filePath: 'pkg:/source/main.brs', + codeSnippet: 'sub main()' + }]); + }); +}); diff --git a/src/debugProtocol/events/responses/ThreadsResponse.ts b/src/debugProtocol/events/responses/ThreadsResponse.ts new file mode 100644 index 00000000..50359748 --- /dev/null +++ b/src/debugProtocol/events/responses/ThreadsResponse.ts @@ -0,0 +1,116 @@ +/* eslint-disable no-bitwise */ +import { SmartBuffer } from 'smart-buffer'; +import type { StopReason } from '../../Constants'; +import { ErrorCode, StopReasonCode } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; + +export class ThreadsResponse { + public static fromJson(data: { + requestId: number; + threads: ThreadInfo[]; + }) { + const response = new ThreadsResponse(); + protocolUtil.loadJson(response, data); + response.data.threads ??= []; + return response; + } + + public static fromBuffer(buffer: Buffer) { + const response = new ThreadsResponse(); + protocolUtil.bufferLoaderHelper(response, buffer, 16, (smartBuffer: SmartBuffer) => { + protocolUtil.loadCommonResponseFields(response, smartBuffer); + + const threadsCount = smartBuffer.readUInt32LE(); // threads_count + + response.data.threads = []; + + // build the list of threads + for (let i = 0; i < threadsCount; i++) { + const thread = {} as ThreadInfo; + const flags = smartBuffer.readUInt8(); + thread.isPrimary = (flags & ThreadInfoFlags.isPrimary) > 0; + thread.stopReason = StopReasonCode[smartBuffer.readUInt32LE()] as StopReason; // stop_reason + thread.stopReasonDetail = protocolUtil.readStringNT(smartBuffer); // stop_reason_detail + thread.lineNumber = smartBuffer.readUInt32LE(); // line_number + thread.functionName = protocolUtil.readStringNT(smartBuffer); // function_name + thread.filePath = protocolUtil.readStringNT(smartBuffer); // file_path + thread.codeSnippet = protocolUtil.readStringNT(smartBuffer); // code_snippet + + response.data.threads.push(thread); + } + }); + return response; + } + + public toBuffer() { + const smartBuffer = new SmartBuffer(); + smartBuffer.writeUInt32LE(this.data.threads?.length ?? 0); // threads_count + for (const thread of this.data.threads ?? []) { + let flags = 0; + flags |= thread.isPrimary ? 1 : 0; + smartBuffer.writeUInt8(flags); //flags + //stop_reason is an 8-bit value (same as the other locations in this protocol); however, it is sent in this response as a 32bit value for historical purposes + smartBuffer.writeUInt32LE(StopReasonCode[thread.stopReason]); // stop_reason + smartBuffer.writeStringNT(thread.stopReasonDetail); // stop_reason_detail + smartBuffer.writeUInt32LE(thread.lineNumber); // line_number + smartBuffer.writeStringNT(thread.functionName); // function_name + smartBuffer.writeStringNT(thread.filePath); // file_path + smartBuffer.writeStringNT(thread.codeSnippet); // code_snippet + } + protocolUtil.insertCommonResponseFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + + public readOffset = 0; + + public data = { + /** + * An array of StrackEntry structs. entries[0] contains the last function called; + * entries[stack_size-1] contains the first function called. + * Debugging clients may reverse the entries to match developer expectations. + */ + threads: undefined as ThreadInfo[], + + // response fields + packetLength: undefined as number, + requestId: undefined as number, + errorCode: ErrorCode.OK + }; +} + +export interface ThreadInfo { + /** + * Indicates whether this thread likely caused the stop or failure + */ + isPrimary: boolean; + /** + * An enum describing why the thread was stopped. + */ + stopReason: StopReason; + /** + * Provides extra details about the stop (for example, "Divide by Zero", "STOP", "BREAK") + */ + stopReasonDetail: string; + /** + * The 1-based line number where the stop or failure occurred. + */ + lineNumber: number; + /** + * The function where the stop or failure occurred. + */ + functionName: string; + /** + * The file where the stop or failure occurred. + */ + filePath: string; + /** + * The code causing the stop or failure. + */ + codeSnippet: string; +} + +enum ThreadInfoFlags { + isPrimary = 0x01 +} diff --git a/src/debugProtocol/events/responses/VariablesResponse.spec.ts b/src/debugProtocol/events/responses/VariablesResponse.spec.ts new file mode 100644 index 00000000..a12eaeb6 --- /dev/null +++ b/src/debugProtocol/events/responses/VariablesResponse.spec.ts @@ -0,0 +1,455 @@ +import type { Variable } from './VariablesResponse'; +import { VariablesResponse, VariableType } from './VariablesResponse'; +import { expect } from 'chai'; +import { ErrorCode } from '../../Constants'; +import { SmartBuffer } from 'smart-buffer'; +import { expectThrows } from '../../../testHelpers.spec'; + +describe('VariablesResponse', () => { + function v(name: string, type: VariableType, value: any, extra?: Record) { + return { + name: name, + type: type, + value: value, + refCount: 1, + isConst: false, + isContainer: false, + ...extra ?? {} + }; + } + + describe('fromJson', () => { + + it('defaults variables array to empty array', () => { + let response = VariablesResponse.fromJson({} as any); + expect(response.data.variables).to.eql([]); + }); + + it('computes isContainer based on variable type', () => { + let response = VariablesResponse.fromJson({ + requestId: 2, + variables: [{ + type: VariableType.AssociativeArray, + children: [] + }, { + type: VariableType.Array, + children: [] + }, { + type: VariableType.List, + children: [] + }, { + type: VariableType.Object, + children: [] + }, { + type: VariableType.SubtypedObject, + children: [] + }] as any[] + }); + expect(response.data.variables.map(x => x.isContainer)).to.eql([ + true, true, true, true, true + ]); + }); + + it('throws if container has no children', () => { + expectThrows(() => { + VariablesResponse.fromJson({ + requestId: 2, + variables: [{ + type: VariableType.AssociativeArray + }] as any[] + }); + }, 'Container variable must have one of these properties defined: childCount, children'); + }); + }); + + describe('readVariable', () => { + it('throws for too-small buffer', () => { + const response = VariablesResponse.fromJson({} as any); + expectThrows(() => { + response['readVariable'](new SmartBuffer()); + }, 'Not enough bytes to create a variable'); + }); + }); + + describe('readVariableValue', () => { + it('returns null for various types', () => { + expect(VariablesResponse.prototype['readVariableValue'](VariableType.Uninitialized, new SmartBuffer())).to.eql(null); + expect(VariablesResponse.prototype['readVariableValue'](VariableType.Unknown, new SmartBuffer())).to.eql(null); + expect(VariablesResponse.prototype['readVariableValue'](VariableType.Invalid, new SmartBuffer())).to.eql(null); + expect(VariablesResponse.prototype['readVariableValue'](VariableType.AssociativeArray, new SmartBuffer())).to.eql(null); + expect(VariablesResponse.prototype['readVariableValue'](VariableType.Array, new SmartBuffer())).to.eql(null); + expect(VariablesResponse.prototype['readVariableValue'](VariableType.List, new SmartBuffer())).to.eql(null); + }); + + it('throws on unknown type', () => { + expectThrows(() => { + VariablesResponse.prototype['readVariableValue']('unknown type' as any, new SmartBuffer()); + }, 'Unable to determine the variable value'); + }); + }); + + describe('flattenVariables', () => { + + it('does not throw for undefined array', () => { + expect( + VariablesResponse.prototype['flattenVariables'](undefined) + ).to.eql([]); + }); + + it('throws for circular reference', () => { + const parent = { children: [] } as Variable; + const child = { children: [] } as Variable; + parent.children.push(child); + child.children.push(parent); + expectThrows(() => { + VariablesResponse.prototype['flattenVariables']([parent, child]); + }, `The variable at index 3 already exists at index 0. You have a circular reference in your variables that needs to be resolved`); + }); + }); + + it('skips var name if missing', () => { + const response = VariablesResponse.fromBuffer( + VariablesResponse.fromJson({ + requestId: 2, + variables: [{ + name: undefined, + refCount: 0, + isConst: false, + isContainer: true, + children: [], + type: VariableType.AssociativeArray, + keyType: VariableType.String, + value: undefined + }] + }).toBuffer() + ); + expect(response.data.variables[0].name).not.to.exist; + expect(response.data.variables[0].refCount).to.eql(0); + }); + + it('handles parent var with children', () => { + let response = VariablesResponse.fromJson({ + requestId: 2, + variables: [{ + name: 'person', + refCount: 2, + isConst: false, + isContainer: true, + type: VariableType.AssociativeArray, + keyType: VariableType.String, + value: undefined, + children: [{ + name: 'firstName', + refCount: 1, + value: 'Bob', + type: VariableType.String, + isContainer: false, + isConst: false + }, { + name: 'lastName', + refCount: 1, + value: undefined, + isContainer: false, + type: VariableType.Invalid, + isConst: false + }] + }] + }); + + expect(response.data).to.eql({ + packetLength: undefined, + errorCode: ErrorCode.OK, + requestId: 2, + variables: [{ + name: 'person', + refCount: 2, + isConst: false, + isContainer: true, + type: VariableType.AssociativeArray, + keyType: 'String', + value: undefined, + children: [{ + name: 'firstName', + refCount: 1, + value: 'Bob', + type: VariableType.String, + isContainer: false, + isConst: false + }, { + name: 'lastName', + refCount: 1, + value: undefined, + isContainer: false, + type: VariableType.Invalid, + isConst: false + }] + }] + }); + + response = VariablesResponse.fromBuffer(response.toBuffer()); + + expect(response.success).to.be.true; + + expect( + response.data + ).to.eql({ + packetLength: 69, // 4 bytes + errorCode: ErrorCode.OK, // 4 bytes + requestId: 2, // 4 bytes + // num_variables // 4 bytes + variables: [{ + // flags // 1 byte + name: 'person', // 7 bytes + refCount: 2, // 4 bytes + isConst: false, // 0 bytes -- part of flags + isContainer: true, // 0 bytes -- part of flags + type: VariableType.AssociativeArray, // 1 byte + keyType: 'String', // 1 byte + // element_count // 4 bytes + children: [{ + // flags // 1 byte + isContainer: false, // 0 bytes --part of flags + isConst: false, // 0 bytes -- part of flags + type: VariableType.String, // 1 byte + name: 'firstName', // 10 bytes + refCount: 1, // 4 bytes + value: 'Bob' // 4 bytes + }, { + // flags // 1 byte + isContainer: false, // 0 bytes -- part of flags + isConst: false, // 0 bytes -- part of flags + type: VariableType.Invalid, // 1 byte + name: 'lastName', // 9 bytes + refCount: 1 // 4 bytes + }] + }] + }); + }); + + it('handles every variable type', () => { + let response = VariablesResponse.fromBuffer( + VariablesResponse.fromJson({ + requestId: 2, + variables: [ + v('a', VariableType.Interface, 'ifArray'), + v('b', VariableType.Object, 'SomeObj'), + v('c', VariableType.String, 'hello world'), + v('d', VariableType.Subroutine, 'main'), + v('e', VariableType.Function, 'test'), + v('f', VariableType.SubtypedObject, 'Parent; Child'), + v('gTrue', VariableType.Boolean, true), + v('gFalse', VariableType.Boolean, false), + v('h', VariableType.Integer, 987), + v('i1', VariableType.LongInteger, 123456789123), + v('i2', VariableType.LongInteger, BigInt(999999999999)), + // v('j', VariableType.Float, 1.987654), // handled in other test since this value is approximated + // v('k', VariableType.Float, 1.2345678912345) // handled in other test since this value is approximated + v('l', VariableType.Uninitialized, undefined), + v('m', VariableType.Unknown, undefined), + v('n', VariableType.Invalid, undefined), + v('o', VariableType.AssociativeArray, undefined), + v('p', VariableType.Array, undefined), + v('q', VariableType.List, undefined) + ] + }).toBuffer() + ); + expect( + response.data.variables.map(x => ({ + name: x.name, + value: x.value, + type: x.type + })) + ).to.eql( + [ + ['a', VariableType.Interface, 'ifArray'], + ['b', VariableType.Object, 'SomeObj'], + ['c', VariableType.String, 'hello world'], + ['d', VariableType.Subroutine, 'main'], + ['e', VariableType.Function, 'test'], + ['f', VariableType.SubtypedObject, 'Parent; Child'], + ['gTrue', VariableType.Boolean, true], + ['gFalse', VariableType.Boolean, false], + ['h', VariableType.Integer, 987], + ['i1', VariableType.LongInteger, BigInt(123456789123)], + ['i2', VariableType.LongInteger, BigInt('999999999999')], + // ['j', VariableType.Float, 1.987654], // handled in other test since this value is approximated + // ['k', VariableType.Float, 1.2345678912345] // handled in other test since this value is approximated + ['l', VariableType.Uninitialized, undefined], + ['m', VariableType.Unknown, undefined], + ['n', VariableType.Invalid, undefined], + ['o', VariableType.AssociativeArray, undefined], + ['p', VariableType.Array, undefined], + ['q', VariableType.List, undefined] + ].map(x => ({ + name: x.shift(), + type: x.shift(), + value: x.shift() + })) + ); + }); + + it('handles float and double', () => { + let response = VariablesResponse.fromBuffer( + VariablesResponse.fromJson({ + requestId: 2, + variables: [ + v('j', VariableType.Float, 1.234567), + v('k', VariableType.Double, 9.87654321987654) + ] + }).toBuffer() + ); + expect(response.data.variables[0].value).to.be.approximately(1.234567, 0.000001); + expect(response.data.variables[1].value).to.be.approximately(9.87654321987654, 0.0000000001); + }); + + it('writes nothing for data that has no value', () => { + const buffer = new SmartBuffer(); + VariablesResponse.prototype['writeVariableValue'](VariableType.Uninitialized, undefined, buffer); + VariablesResponse.prototype['writeVariableValue'](VariableType.Unknown, undefined, buffer); + VariablesResponse.prototype['writeVariableValue'](VariableType.Invalid, undefined, buffer); + VariablesResponse.prototype['writeVariableValue'](VariableType.AssociativeArray, undefined, buffer); + VariablesResponse.prototype['writeVariableValue'](VariableType.Array, undefined, buffer); + VariablesResponse.prototype['writeVariableValue'](VariableType.List, undefined, buffer); + expect(buffer.length).to.eql(0); + }); + + it('writeVariableValue throws for unknown variable value', () => { + expectThrows( + () => VariablesResponse.prototype['writeVariableValue']('NotRealVariableType' as any, undefined, new SmartBuffer()), + 'Unable to determine the variable value' + ); + }); + + it('writeVariableValue throws for incorrectly formatted SubtypedObject', () => { + expectThrows( + () => VariablesResponse.prototype['writeVariableValue'](VariableType.SubtypedObject, 'IShouldHaveASemicolonAndAnotherThingAfterThat', new SmartBuffer()), + 'Expected two names for subtyped object' + ); + }); + + it('writeVariableValue throws for undefined SubtypedObject', () => { + expectThrows( + () => VariablesResponse.prototype['writeVariableValue'](VariableType.SubtypedObject, undefined, new SmartBuffer()), + 'Expected two names for subtyped object' + ); + }); + + it('writeVariable determines if variable is const', () => { + let response = VariablesResponse.fromBuffer( + VariablesResponse.fromJson({ + requestId: 2, + variables: [ + v('alpha', VariableType.List, undefined, { isConst: true }), + v('beta', VariableType.List, undefined, { isConst: false }) + + ] + }).toBuffer() + ); + expect(response.data.variables[0].isConst).to.be.true; + expect(response.data.variables[1].isConst).to.be.false; + }); + + it('handles several root-level vars', () => { + let response = VariablesResponse.fromJson({ + requestId: 2, + variables: [{ + name: 'm', + refCount: 2, + isConst: false, + isContainer: true, + childCount: 3, + type: VariableType.AssociativeArray, + keyType: VariableType.String, + value: undefined + }, { + name: 'nodes', + refCount: 2, + isConst: false, + isContainer: true, + childCount: 2, + type: VariableType.Array, + keyType: VariableType.Integer, + value: undefined + }, { + name: 'message', + refCount: 2, + isConst: false, + isContainer: false, + type: VariableType.String, + value: 'hello' + }] + }); + + expect(response.data).to.eql({ + packetLength: undefined, + errorCode: ErrorCode.OK, + requestId: 2, + variables: [{ + isConst: false, + isContainer: true, + type: VariableType.AssociativeArray, + name: 'm', + refCount: 2, + keyType: VariableType.String, + childCount: 3, + value: undefined + }, { + isConst: false, + isContainer: true, + type: VariableType.Array, + name: 'nodes', + refCount: 2, + keyType: VariableType.Integer, + childCount: 2, + value: undefined + }, { + isConst: false, + isContainer: false, + type: VariableType.String, + name: 'message', + refCount: 2, + value: 'hello' + }] + }); + + response = VariablesResponse.fromBuffer(response.toBuffer()); + + expect(response.success).to.be.true; + + expect( + response.data + ).to.eql({ + packetLength: 66, // 4 bytes + errorCode: ErrorCode.OK, // 4 bytes + requestId: 2, // 4 bytes + // num_variables // 4 bytes + variables: [{ + // flags // 1 byte + isConst: false, // 0 bytes -- part of flags + isContainer: true, // 0 bytes -- part of flags + type: VariableType.AssociativeArray, // 1 byte + name: 'm', // 2 bytes + refCount: 2, // 4 bytes + keyType: VariableType.String, // 1 byte + childCount: 3 // 4 bytes + }, { + // flags // 1 byte + isConst: false, // 0 bytes -- part of flags + isContainer: true, // 0 bytes -- part of flags + type: VariableType.Array, // 1 byte + name: 'nodes', // 6 bytes + refCount: 2, // 4 bytes + keyType: VariableType.Integer, // 1 byte + childCount: 2 // 4 bytes + }, { + // flags // 1 byte + isConst: false, // 0 bytes -- part of flags + isContainer: false, // 0 bytes -- part of flags + type: VariableType.String, // 1 byte + name: 'message', // 8 bytes + refCount: 2, // 4 bytes + value: 'hello' // 6 bytes + }] + }); + }); +}); diff --git a/src/debugProtocol/events/responses/VariablesResponse.ts b/src/debugProtocol/events/responses/VariablesResponse.ts new file mode 100644 index 00000000..f8d02777 --- /dev/null +++ b/src/debugProtocol/events/responses/VariablesResponse.ts @@ -0,0 +1,404 @@ +/* eslint-disable no-bitwise */ +import { SmartBuffer } from 'smart-buffer'; +import { util } from '../../../util'; +import { ErrorCode } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolResponse, ProtocolResponseData, ProtocolResponseErrorData } from '../ProtocolEvent'; + +export class VariablesResponse implements ProtocolResponse { + + public static fromJson(data: { + requestId: number; + variables: Variable[]; + errorData?: ProtocolResponseErrorData; + }) { + const response = new VariablesResponse(); + protocolUtil.loadJson(response, data); + response.data.variables ??= []; + //validate that any object marked as `isContainer` either has an array of children or has an element count + for (const variable of response.flattenVariables(response.data.variables)) { + const hasChildrenArray = Array.isArray(variable.children); + if (variable.childCount > 0 || hasChildrenArray) { + variable.isContainer = true; + } + if (hasChildrenArray) { + delete variable.childCount; + } + if (util.isNullish(variable.isContainer)) { + variable.isContainer = [VariableType.AssociativeArray, VariableType.Array, VariableType.List, VariableType.Object, VariableType.SubtypedObject].includes(variable.type); + } + if (variable.isContainer && util.isNullish(variable.childCount) && !hasChildrenArray) { + throw new Error('Container variable must have one of these properties defined: childCount, children'); + } + } + return response; + } + + public static fromBuffer(buffer: Buffer) { + const response = new VariablesResponse(); + protocolUtil.bufferLoaderHelper(response, buffer, 12, (smartBuffer: SmartBuffer) => { + protocolUtil.loadCommonResponseFields(response, smartBuffer); + const numVariables = smartBuffer.readUInt32LE(); // num_variables + + + const variables: Array = []; + let latestContainer: Variable; + let variableCount = 0; + // build the list of BreakpointInfo + for (let i = 0; i < numVariables; i++) { + const variable = response.readVariable(smartBuffer); + variableCount++; + if (variable.isChildKey === false) { + latestContainer = variable as any; + delete variable.childCount; + latestContainer.children = []; + variables.push(variable); + } else if (latestContainer) { + latestContainer.children.push(variable); + } else { + variables.push(variable); + } + delete variable.isChildKey; + } + response.data.variables = variables; + + return variableCount === numVariables; + }); + return response; + } + + private readVariable(smartBuffer: SmartBuffer): Variable & { isChildKey: boolean } { + if (smartBuffer.length < 13) { + throw new Error('Not enough bytes to create a variable'); + } + const variable = {} as Variable & { isChildKey: boolean }; + const flags = smartBuffer.readUInt8(); + + // Determine the different variable properties + variable.isChildKey = (flags & VariableFlags.isChildKey) > 0; + variable.isConst = (flags & VariableFlags.isConst) > 0; + variable.isContainer = (flags & VariableFlags.isContainer) > 0; + const isNameHere = (flags & VariableFlags.isNameHere) > 0; + const isRefCounted = (flags & VariableFlags.isRefCounted) > 0; + const isValueHere = (flags & VariableFlags.isValueHere) > 0; + const variableTypeCode = smartBuffer.readUInt8(); + variable.type = VariableTypeCode[variableTypeCode] as VariableType; // variable_type + + if (isNameHere) { + // we have a name. Pull it out of the buffer. + variable.name = protocolUtil.readStringNT(smartBuffer); //name + } + + if (isRefCounted) { + // This variables reference counts are tracked and we can pull it from the buffer. + variable.refCount = smartBuffer.readUInt32LE(); + } else { + variable.refCount = 0; + } + + if (variable.isContainer) { + // It is a form of container object. + // Are the key strings or integers for example + variable.keyType = VariableTypeCode[smartBuffer.readUInt8()] as VariableType; + // Equivalent to length on arrays + variable.childCount = smartBuffer.readUInt32LE(); + } + + if (isValueHere) { + // Pull out the variable data based on the type if that type returns a value + variable.value = this.readVariableValue(variable.type, smartBuffer); + } + return variable; + } + + private readVariableValue(variableType: VariableType, smartBuffer: SmartBuffer) { + switch (variableType) { + case VariableType.Interface: + case VariableType.Object: + case VariableType.String: + case VariableType.Subroutine: + case VariableType.Function: + return protocolUtil.readStringNT(smartBuffer); + case VariableType.SubtypedObject: + let names = []; + for (let i = 0; i < 2; i++) { + names.push(protocolUtil.readStringNT(smartBuffer)); + } + return names.join('; '); + case VariableType.Boolean: + return smartBuffer.readUInt8() > 0; + case VariableType.Integer: + return smartBuffer.readInt32LE(); + case VariableType.LongInteger: + return smartBuffer.readBigInt64LE(); + case VariableType.Float: + return smartBuffer.readFloatLE(); + case VariableType.Double: + return smartBuffer.readDoubleLE(); + case VariableType.Uninitialized: + case VariableType.Unknown: + case VariableType.Invalid: + case VariableType.AssociativeArray: + case VariableType.Array: + case VariableType.List: + return null; + default: + throw new Error('Unable to determine the variable value'); + } + } + + private flattenVariables(variables: Variable[]) { + //flatten the variables + const result = [] as Variable[]; + for (let rootVariable of variables ?? []) { + result.push(rootVariable); + //add all child variables to the array + for (const child of rootVariable.children ?? []) { + result.push(child); + } + } + //catch duplicates and circular references + for (let i = 0; i < result.length; i++) { + const idx = result.indexOf(result[i], i + 1); + if (idx > -1) { + throw new Error(`The variable at index ${idx} already exists at index ${i}. You have a circular reference in your variables that needs to be resolved`); + } + } + return result; + } + + public toBuffer() { + const smartBuffer = new SmartBuffer(); + const variables = this.flattenVariables(this.data.variables); + smartBuffer.writeUInt32LE(variables.length); // num_variables + for (const variable of variables) { + this.writeVariable(variable, smartBuffer); + } + protocolUtil.insertCommonResponseFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + private writeVariable(variable: Variable, smartBuffer: SmartBuffer) { + + let flags = 0; + //variables that have children are NOT child keys themselves + flags |= Array.isArray(variable.children) ? 0 : VariableFlags.isChildKey; + flags |= variable.isConst ? VariableFlags.isConst : 0; + flags |= variable.isContainer ? VariableFlags.isContainer : 0; + + const isNameHere = !util.isNullish(variable.name); + flags |= isNameHere ? VariableFlags.isNameHere : 0; + + const isRefCounted = variable.refCount > 0; + flags |= isRefCounted ? VariableFlags.isRefCounted : 0; + + const isValueHere = !util.isNullish(variable.value); + flags |= isValueHere ? VariableFlags.isValueHere : 0; + + smartBuffer.writeUInt8(flags); //flags + smartBuffer.writeUInt8(VariableTypeCode[variable.type] as number); // variable_type + + if (isNameHere) { + smartBuffer.writeStringNT(variable.name); //name + } + + if (isRefCounted) { + smartBuffer.writeUInt32LE(variable.refCount); //ref_count + } + + if (variable.isContainer) { + smartBuffer.writeUInt8(VariableTypeCode[variable.keyType] as number); // key_type + // Equivalent to .length on arrays + smartBuffer.writeUInt32LE( + variable.children?.length ?? variable.childCount + ); // element_count + } + + if (isValueHere) { + // write the variable data based on the type + this.writeVariableValue(variable.type, variable.value, smartBuffer); + } + return variable; + } + + + private writeVariableValue(variableType: VariableType, value: any, smartBuffer: SmartBuffer): void { + switch (variableType) { + case VariableType.Interface: + case VariableType.Object: + case VariableType.String: + case VariableType.Subroutine: + case VariableType.Function: + smartBuffer.writeStringNT(value as string); + break; + case VariableType.SubtypedObject: + const names = (value as string ?? '').split('; '); + if (names.length !== 2) { + throw new Error('Expected two names for subtyped object'); + } + for (const name of names) { + smartBuffer.writeStringNT(name); + } + break; + case VariableType.Boolean: + smartBuffer.writeUInt8(value === true ? 1 : 0); + break; + case VariableType.Integer: + smartBuffer.writeInt32LE(value as number); + break; + case VariableType.LongInteger: + smartBuffer.writeBigInt64LE( + typeof value === 'bigint' ? value : BigInt(value as number) + ); + break; + case VariableType.Float: + smartBuffer.writeFloatLE(value as number); + break; + case VariableType.Double: + smartBuffer.writeDoubleLE(value as number); + break; + case VariableType.Uninitialized: + case VariableType.Unknown: + case VariableType.Invalid: + case VariableType.AssociativeArray: + case VariableType.Array: + case VariableType.List: + break; + default: + throw new Error('Unable to determine the variable value'); + } + } + + public success = false; + + public readOffset = 0; + + public data = { + packetLength: undefined as number, + errorCode: ErrorCode.OK + } as VariablesResponseData; +} +export interface VariablesResponseData extends ProtocolResponseData { + variables: Variable[]; +} + +export enum VariableFlags { + /** + * value is a child of the requested variable + * e.g., an element of an array or field of an AA + */ + isChildKey = 1, + /** + * value is constant + */ + isConst = 2, + /** + * The referenced value is a container (e.g., a list or array) + */ + isContainer = 4, + /** + * The name is included in this VariableInfo + */ + isNameHere = 8, + /** + * value is reference-counted. + */ + isRefCounted = 16, + /** + * value is included in this VariableInfo + */ + isValueHere = 32, + /** + * Value is container, key lookup is case sensitive + * @since protocol 3.1.0 + */ + isKeysCaseSensitive = 64 +} + +/** + * Every type of variable supported by the protocol + */ +export enum VariableType { + AssociativeArray = 'AssociativeArray', + Array = 'Array', + Boolean = 'Boolean', + Double = 'Double', + Float = 'Float', + Function = 'Function', + Integer = 'Integer', + Interface = 'Interface', + Invalid = 'Invalid', + List = 'List', + LongInteger = 'LongInteger', + Object = 'Object', + String = 'String', + Subroutine = 'Subroutine', + SubtypedObject = 'SubtypedObject', + Uninitialized = 'Uninitialized', + Unknown = 'Unknown' +} + +/** + * An enum used to convert VariableType strings to their protocol integer value + */ +enum VariableTypeCode { + AssociativeArray = 1, + Array = 2, + Boolean = 3, + Double = 4, + Float = 5, + Function = 6, + Integer = 7, + Interface = 8, + Invalid = 9, + List = 10, + LongInteger = 11, + Object = 12, + String = 13, + Subroutine = 14, + SubtypedObject = 15, + Uninitialized = 16, + Unknown = 17 +} + +export interface Variable { + /** + * 0 means this var isn't refCounted, and thus `refCount` will be omitted from reading and writing to buffer + */ + refCount: number; + /** + * I think this means "is this mutatable". Like an object would be isConst=false, but "some string" can only be replaced by a totally new string? But don't quote me on this + */ + isConst: boolean; + /** + * A type-dependent value based on the `variableType` field. It is not present for all types + */ + value: string | number | bigint | boolean | null; + /** + * The type of variable or value. + */ + type: VariableType; + /** + * The variable name. `undefined` means there was no variable name available + */ + name?: string; + /** + * If this variable is a container, what variable type are its keys? (integer for array, string for AA, etc...). + * TODO can we get roku to narrow this a bit? + */ + keyType?: VariableType; + /** + * Is this variable a container var (i.e. an array or object with children) + */ + isContainer: boolean; + /** + * If the variable is a container, it will have child elements. this is the number of those children. If `.children` is set, this field will be set to undefined + * (meaning it will be ignored during serialization) + */ + childCount?: number; + /** + * The full list of children for this variable. The entire Variable response may not be more than 2 total levels deep. + * (i.e. `parent` -> `children[]`). Children may not have additional children, those would need to be resolve using subsequent `variables` requests. + */ + children?: Variable[]; +} diff --git a/src/debugProtocol/events/updates/AllThreadsStoppedUpdate.spec.ts b/src/debugProtocol/events/updates/AllThreadsStoppedUpdate.spec.ts new file mode 100644 index 00000000..26f8047e --- /dev/null +++ b/src/debugProtocol/events/updates/AllThreadsStoppedUpdate.spec.ts @@ -0,0 +1,37 @@ +import { expect } from 'chai'; +import { ErrorCode, StopReason, UpdateType } from '../../Constants'; +import { AllThreadsStoppedUpdate } from './AllThreadsStoppedUpdate'; + +describe('AllThreadsStoppedUpdate', () => { + it('serializes and deserializes properly', () => { + const command = AllThreadsStoppedUpdate.fromJson({ + threadIndex: 1, + stopReason: StopReason.Break, + stopReasonDetail: 'because' + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 0, + errorCode: ErrorCode.OK, + updateType: UpdateType.AllThreadsStopped, + + threadIndex: 1, + stopReason: StopReason.Break, + stopReasonDetail: 'because' + }); + + expect( + AllThreadsStoppedUpdate.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 29, // 4 bytes + requestId: 0, // 4 bytes + errorCode: ErrorCode.OK, // 4 bytes + updateType: UpdateType.AllThreadsStopped, // 4 bytes + + threadIndex: 1, // 4 bytes + stopReason: StopReason.Break, // 1 bytes + stopReasonDetail: 'because' // 8 bytes + }); + }); +}); diff --git a/src/debugProtocol/events/updates/AllThreadsStoppedUpdate.ts b/src/debugProtocol/events/updates/AllThreadsStoppedUpdate.ts new file mode 100644 index 00000000..969cf9ad --- /dev/null +++ b/src/debugProtocol/events/updates/AllThreadsStoppedUpdate.ts @@ -0,0 +1,67 @@ +import { SmartBuffer } from 'smart-buffer'; +import type { StopReason } from '../../Constants'; +import { ErrorCode, StopReasonCode, UpdateType } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolUpdate } from '../ProtocolEvent'; + +/** + * All threads are stopped and an ALL_THREADS_STOPPED message is sent to the debugging client. + * + * The data field includes information on why the threads were stopped. + */ +export class AllThreadsStoppedUpdate implements ProtocolUpdate { + + public static fromJson(data: { + threadIndex: number; + stopReason: StopReason; + stopReasonDetail: string; + }) { + const update = new AllThreadsStoppedUpdate(); + protocolUtil.loadJson(update, data); + return update; + } + + public static fromBuffer(buffer: Buffer) { + const update = new AllThreadsStoppedUpdate(); + protocolUtil.bufferLoaderHelper(update, buffer, 16, (smartBuffer) => { + protocolUtil.loadCommonUpdateFields(update, smartBuffer, update.data.updateType); + + update.data.threadIndex = smartBuffer.readInt32LE(); + update.data.stopReason = StopReasonCode[smartBuffer.readUInt8()] as StopReason; + update.data.stopReasonDetail = protocolUtil.readStringNT(smartBuffer); + }); + return update; + } + + public toBuffer() { + let smartBuffer = new SmartBuffer(); + + smartBuffer.writeInt32LE(this.data.threadIndex); // primary_thread_index + smartBuffer.writeUInt8(StopReasonCode[this.data.stopReason]); // stop_reason + smartBuffer.writeStringNT(this.data.stopReasonDetail); //stop_reason_detail + + protocolUtil.insertCommonUpdateFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + /** + * The index of the primary thread that triggered the stop + */ + threadIndex: undefined as number, + stopReason: undefined as StopReason, + stopReasonDetail: undefined as string, + + //common props + packetLength: undefined as number, + requestId: 0, //all updates have requestId === 0 + errorCode: ErrorCode.OK, + updateType: UpdateType.AllThreadsStopped + }; +} diff --git a/src/debugProtocol/events/updates/BreakpointErrorUpdate.spec.ts b/src/debugProtocol/events/updates/BreakpointErrorUpdate.spec.ts new file mode 100644 index 00000000..311d8597 --- /dev/null +++ b/src/debugProtocol/events/updates/BreakpointErrorUpdate.spec.ts @@ -0,0 +1,103 @@ +import { expect } from 'chai'; +import { ErrorCode, UpdateType } from '../../Constants'; +import { BreakpointErrorUpdate } from './BreakpointErrorUpdate'; + +describe('BreakpointErrorUpdate', () => { + it('serializes and deserializes properly', () => { + const command = BreakpointErrorUpdate.fromJson({ + breakpointId: 3, + compileErrors: [ + 'compile 1' + ], + runtimeErrors: [ + 'runtime 1' + ], + otherErrors: [ + 'other 1' + ] + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 0, + errorCode: ErrorCode.OK, + updateType: UpdateType.BreakpointError, + + breakpointId: 3, + compileErrors: [ + 'compile 1' + ], + runtimeErrors: [ + 'runtime 1' + ], + otherErrors: [ + 'other 1' + ] + }); + + expect( + BreakpointErrorUpdate.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 64, // 4 bytes + requestId: 0, // 4 bytes + errorCode: ErrorCode.OK, // 4 bytes + updateType: UpdateType.BreakpointError, // 4 bytes + + //flags // 4 bytes + + breakpointId: 3, // 4 bytes + // num_compile_errors // 4 bytes + compileErrors: [ + 'compile 1' // 10 bytes + ], + // num_runtime_errors // 4 bytes + runtimeErrors: [ + 'runtime 1' // 10 bytes + ], + // num_other_errors // 4 bytes + otherErrors: [ + 'other 1' // 8 bytes + ] + }); + }); + + it('Handles zero errors', () => { + const command = BreakpointErrorUpdate.fromJson({ + breakpointId: 3, + compileErrors: [], + runtimeErrors: [], + otherErrors: [] + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 0, + errorCode: ErrorCode.OK, + updateType: UpdateType.BreakpointError, + + breakpointId: 3, + compileErrors: [], + runtimeErrors: [], + otherErrors: [] + }); + + expect( + BreakpointErrorUpdate.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 36, // 4 bytes + requestId: 0, // 4 bytes + errorCode: ErrorCode.OK, // 4 bytes + updateType: UpdateType.BreakpointError, // 4 bytes + + //flags // 4 bytes + + breakpointId: 3, // 4 bytes + // num_compile_errors // 4 bytes + compileErrors: [], + // num_runtime_errors // 4 bytes + runtimeErrors: [], + // num_other_errors // 4 bytes + otherErrors: [] + }); + }); +}); diff --git a/src/debugProtocol/events/updates/BreakpointErrorUpdate.ts b/src/debugProtocol/events/updates/BreakpointErrorUpdate.ts new file mode 100644 index 00000000..e135c7c3 --- /dev/null +++ b/src/debugProtocol/events/updates/BreakpointErrorUpdate.ts @@ -0,0 +1,115 @@ +import { SmartBuffer } from 'smart-buffer'; +import { ErrorCode, UpdateType } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; + +/** + * Data sent as the data segment of message type: BREAKPOINT_ERROR + ``` + struct BreakpointErrorUpdateData { + uint32 flags; // Always 0, reserved for future use + uint32 breakpoint_id; + uint32 num_compile_errors; + utf8z[num_compile_errors] compile_errors; + uint32 num_runtime_errors; + utf8z[num_runtime_errors] runtime_errors; + uint32 num_other_errors; // E.g., permissions errors + utf8z[num_other_errors] other_errors; + } + ``` +*/ +export class BreakpointErrorUpdate { + + public static fromJson(data: { + breakpointId: number; + compileErrors: string[]; + runtimeErrors: string[]; + otherErrors: string[]; + }) { + const update = new BreakpointErrorUpdate(); + protocolUtil.loadJson(update, data); + update.data.compileErrors ??= []; + update.data.runtimeErrors ??= []; + update.data.otherErrors ??= []; + return update; + } + + public static fromBuffer(buffer: Buffer) { + const update = new BreakpointErrorUpdate(); + protocolUtil.bufferLoaderHelper(update, buffer, 20, (smartBuffer) => { + protocolUtil.loadCommonUpdateFields(update, smartBuffer, update.data.updateType); + + smartBuffer.readUInt32LE(); // flags - always 0, reserved for future use + update.data.breakpointId = smartBuffer.readUInt32LE(); // breakpoint_id + + const compileErrorCount = smartBuffer.readUInt32LE(); // num_compile_errors + update.data.compileErrors = []; + for (let i = 0; i < compileErrorCount; i++) { + update.data.compileErrors.push( + protocolUtil.readStringNT(smartBuffer) + ); + } + + const runtimeErrorCount = smartBuffer.readUInt32LE(); // num_runtime_errors + update.data.runtimeErrors = []; + for (let i = 0; i < runtimeErrorCount; i++) { + update.data.runtimeErrors.push( + protocolUtil.readStringNT(smartBuffer) + ); + } + + const otherErrorCount = smartBuffer.readUInt32LE(); // num_other_errors + update.data.otherErrors = []; + for (let i = 0; i < otherErrorCount; i++) { + update.data.otherErrors.push( + protocolUtil.readStringNT(smartBuffer) + ); + } + }); + return update; + } + + public toBuffer() { + let smartBuffer = new SmartBuffer(); + + smartBuffer.writeInt32LE(0); // flags - always 0, reserved for future use + smartBuffer.writeUInt32LE(this.data.breakpointId); // breakpoint_id + + smartBuffer.writeUInt32LE(this.data.compileErrors?.length ?? 0); // num_compile_errors + for (let error of this.data.compileErrors ?? []) { + smartBuffer.writeStringNT(error); + } + + smartBuffer.writeUInt32LE(this.data.runtimeErrors?.length ?? 0); // num_runtime_errors + for (let error of this.data.runtimeErrors ?? []) { + smartBuffer.writeStringNT(error); + } + + smartBuffer.writeUInt32LE(this.data.otherErrors?.length ?? 0); // num_other_errors + for (let error of this.data.otherErrors ?? []) { + smartBuffer.writeStringNT(error); + } + + protocolUtil.insertCommonUpdateFields(this, smartBuffer); + + return smartBuffer.toBuffer(); + } + + public success = false; + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + breakpointId: undefined as number, + compileErrors: undefined as string[], + runtimeErrors: undefined as string[], + otherErrors: undefined as string[], + + //common props + packetLength: undefined as number, + requestId: 0, //all updates have requestId === 0 + errorCode: ErrorCode.OK, + updateType: UpdateType.BreakpointError + }; +} diff --git a/src/debugProtocol/events/updates/BreakpointVerifiedUpdate.spec.ts b/src/debugProtocol/events/updates/BreakpointVerifiedUpdate.spec.ts new file mode 100644 index 00000000..35f17dd1 --- /dev/null +++ b/src/debugProtocol/events/updates/BreakpointVerifiedUpdate.spec.ts @@ -0,0 +1,46 @@ +import { expect } from 'chai'; +import { ErrorCode, UpdateType } from '../../Constants'; +import { BreakpointVerifiedUpdate } from './BreakpointVerifiedUpdate'; + +describe('BreakpointVerifiedUpdate', () => { + it('serializes and deserializes properly', () => { + const command = BreakpointVerifiedUpdate.fromJson({ + breakpoints: [{ + id: 2 + }, { + id: 1 + }] + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 0, + errorCode: ErrorCode.OK, + updateType: UpdateType.BreakpointVerified, + + breakpoints: [{ + id: 2 + }, { + id: 1 + }] + }); + + expect( + BreakpointVerifiedUpdate.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 32, // 4 bytes + requestId: 0, // 4 bytes + errorCode: ErrorCode.OK, // 4 bytes + updateType: UpdateType.BreakpointVerified, // 4 bytes + + //flags: 0 // 4 bytes + + //num_breakpoints // 4 bytes + breakpoints: [{ + id: 2 // 4 bytes + }, { + id: 1 // 4 bytes + }] + }); + }); +}); diff --git a/src/debugProtocol/events/updates/BreakpointVerifiedUpdate.ts b/src/debugProtocol/events/updates/BreakpointVerifiedUpdate.ts new file mode 100644 index 00000000..2887ba33 --- /dev/null +++ b/src/debugProtocol/events/updates/BreakpointVerifiedUpdate.ts @@ -0,0 +1,84 @@ +import { SmartBuffer } from 'smart-buffer'; +import type { StopReason } from '../../Constants'; +import { ErrorCode, StopReasonCode, UpdateType } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolUpdate } from '../ProtocolEvent'; + +/** +``` +// Data sent as the data segment of message type: BREAKPOINT_VERIFIED +struct BreakpointVerifiedUpdateData { + uint32 flags // Always 0, reserved for future use + uint32 num_breakpoints + BreakpointVerifiedInfo[num_breakpoints] breakpoint_verified_info +} + +struct BreakpointVerifiedInfo { + uint32 breakpoint_id +} +``` +*/ +export class BreakpointVerifiedUpdate { + + public static fromJson(data: { + breakpoints: VerifiedBreakpoint[]; + }) { + const update = new BreakpointVerifiedUpdate(); + protocolUtil.loadJson(update, data); + return update; + } + + public static fromBuffer(buffer: Buffer) { + const update = new BreakpointVerifiedUpdate(); + protocolUtil.bufferLoaderHelper(update, buffer, 16, (smartBuffer) => { + protocolUtil.loadCommonUpdateFields(update, smartBuffer, update.data.updateType); + + const flags = smartBuffer.readUInt32LE(); // Always 0, reserved for future use + const breakpointCount = smartBuffer.readUInt32LE(); // num_breakpoints + update.data.breakpoints = []; + for (let i = 0; i < breakpointCount; i++) { + update.data.breakpoints.push({ + id: smartBuffer.readUInt32LE() // uint32 breakpoint_id + }); + } + }); + return update; + } + + public toBuffer() { + let smartBuffer = new SmartBuffer(); + + smartBuffer.writeUInt32LE(0); // flags (Always 0, reserved for future use) + const breakpoints = this.data?.breakpoints ?? []; + smartBuffer.writeUInt32LE(breakpoints.length); // num_breakpoints + for (const breakpoint of breakpoints) { + smartBuffer.writeUInt32LE(breakpoint.id ?? 0); //breakpoint_id + } + + protocolUtil.insertCommonUpdateFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + /** + * The index of the primary thread that triggered the stop + */ + breakpoints: undefined as VerifiedBreakpoint[], + + //common props + packetLength: undefined as number, + requestId: 0, //all updates have requestId === 0 + errorCode: ErrorCode.OK, + updateType: UpdateType.BreakpointVerified + }; +} + +export interface VerifiedBreakpoint { + id: number; +} diff --git a/src/debugProtocol/events/updates/CompileErrorUpdate.spec.ts b/src/debugProtocol/events/updates/CompileErrorUpdate.spec.ts new file mode 100644 index 00000000..67e646d3 --- /dev/null +++ b/src/debugProtocol/events/updates/CompileErrorUpdate.spec.ts @@ -0,0 +1,42 @@ +import { expect } from 'chai'; +import { ErrorCode, UpdateType } from '../../Constants'; +import { CompileErrorUpdate } from './CompileErrorUpdate'; + +describe('CompileErrorUpdate', () => { + it('serializes and deserializes properly', () => { + const command = CompileErrorUpdate.fromJson({ + errorMessage: 'crashed', + filePath: 'pkg:/source/main.brs', + libraryName: 'complib1', + lineNumber: 3 + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 0, + errorCode: ErrorCode.OK, + updateType: UpdateType.CompileError, + + errorMessage: 'crashed', + filePath: 'pkg:/source/main.brs', + libraryName: 'complib1', + lineNumber: 3, + flags: undefined + }); + + expect( + CompileErrorUpdate.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 62, // 4 bytes + requestId: 0, // 4 bytes + errorCode: ErrorCode.OK, // 4 bytes + updateType: UpdateType.CompileError, // 4 bytes + + errorMessage: 'crashed', // 8 bytes + filePath: 'pkg:/source/main.brs', // 21 bytes + libraryName: 'complib1', // 9 bytes + lineNumber: 3, // 4 bytes + flags: 0 // 4 bytes + }); + }); +}); diff --git a/src/debugProtocol/events/updates/CompileErrorUpdate.ts b/src/debugProtocol/events/updates/CompileErrorUpdate.ts new file mode 100644 index 00000000..4d700f44 --- /dev/null +++ b/src/debugProtocol/events/updates/CompileErrorUpdate.ts @@ -0,0 +1,96 @@ +import { SmartBuffer } from 'smart-buffer'; +import { ErrorCode, UpdateType } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; + +/** + * A COMPILE_ERROR is sent if a compilation error occurs. In this case, the update_type field in a DebuggerUpdate message is set to + * COMPILE_ERROR, and the data field contains a structure named CompileErrorUpdateData that provides the reason for the error. + * The CompileErrorUpdateData structure has the following syntax: + ``` + struct CompileErrorUpdateData { + uint32 flags; // Always 0, reserved for future use + utf8z error_string; + utf8z file_spec; + uint32 line_number; + utf8z library_name; + } + ``` +*/ +export class CompileErrorUpdate { + + public static fromJson(data: { + errorMessage: string; + filePath: string; + lineNumber: number; + libraryName: string; + }) { + const update = new CompileErrorUpdate(); + protocolUtil.loadJson(update, data); + return update; + } + + public static fromBuffer(buffer: Buffer) { + const update = new CompileErrorUpdate(); + protocolUtil.bufferLoaderHelper(update, buffer, 20, (smartBuffer) => { + protocolUtil.loadCommonUpdateFields(update, smartBuffer, update.data.updateType); + update.data.flags = smartBuffer.readUInt32LE(); // flags - always 0, reserved for future use + update.data.errorMessage = protocolUtil.readStringNT(smartBuffer); // error_string + update.data.filePath = protocolUtil.readStringNT(smartBuffer); // file_spec + update.data.lineNumber = smartBuffer.readUInt32LE(); // line_number + update.data.libraryName = protocolUtil.readStringNT(smartBuffer); // library_name + }); + return update; + } + + public toBuffer() { + let smartBuffer = new SmartBuffer(); + + smartBuffer.writeUInt32LE(this.data.flags ?? 0); // flags + smartBuffer.writeStringNT(this.data.errorMessage); // error_string + smartBuffer.writeStringNT(this.data.filePath); // file_spec + smartBuffer.writeUInt32LE(this.data.lineNumber); // line_number + smartBuffer.writeStringNT(this.data.libraryName); // library_name + + protocolUtil.insertCommonUpdateFields(this, smartBuffer); + + return smartBuffer.toBuffer(); + } + + public success = false; + /** + * How many bytes were read by the `fromBuffer` method. Only populated when constructed by `fromBuffer` + */ + public readOffset: number = undefined; + + public data = { + flags: undefined as number, + /** + * A text message describing the compiler error. + * + * This is completely unrelated to the DebuggerUpdate.errorCode field. + */ + errorMessage: undefined as string, + /** + * A simple file path indicating where the compiler error occurred. It maps to all matching file paths in the channel or its libraries + * + * `"pkg:/"` specifies a file in the channel + * + * `"lib://"` specifies a file in a library. + */ + filePath: undefined as string, + /** + * The 1-based line number where the compile error occurred. + */ + lineNumber: undefined as number, + /** + * The name of the library where the compile error occurred. + */ + libraryName: undefined as string, + + //common props + packetLength: undefined as number, + requestId: 0, //all updates have requestId === 0 + errorCode: ErrorCode.OK, + updateType: UpdateType.CompileError + }; +} diff --git a/src/debugProtocol/events/updates/IOPortOpenedUpdate.spec.ts b/src/debugProtocol/events/updates/IOPortOpenedUpdate.spec.ts new file mode 100644 index 00000000..7e6feab1 --- /dev/null +++ b/src/debugProtocol/events/updates/IOPortOpenedUpdate.spec.ts @@ -0,0 +1,31 @@ +import { expect } from 'chai'; +import { ErrorCode, StopReasonCode, UpdateType } from '../../Constants'; +import { IOPortOpenedUpdate } from './IOPortOpenedUpdate'; + +describe('IOPortOpenedUpdate', () => { + it('serializes and deserializes properly', () => { + const command = IOPortOpenedUpdate.fromJson({ + port: 1234 + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 0, + errorCode: ErrorCode.OK, + updateType: UpdateType.IOPortOpened, + + port: 1234 + }); + + expect( + IOPortOpenedUpdate.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 20, // 4 bytes + requestId: 0, // 4 bytes + errorCode: ErrorCode.OK, // 4 bytes + updateType: UpdateType.IOPortOpened, // 4 bytes + + port: 1234 // 4 bytes + }); + }); +}); diff --git a/src/debugProtocol/events/updates/IOPortOpenedUpdate.ts b/src/debugProtocol/events/updates/IOPortOpenedUpdate.ts new file mode 100644 index 00000000..963d3d31 --- /dev/null +++ b/src/debugProtocol/events/updates/IOPortOpenedUpdate.ts @@ -0,0 +1,55 @@ +import { SmartBuffer } from 'smart-buffer'; +import { ErrorCode, UpdateType } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; +import type { ProtocolUpdate, ProtocolResponse, ProtocolRequest } from '../ProtocolEvent'; + +export class IOPortOpenedUpdate { + + public static fromJson(data: { + port: number; + }) { + const update = new IOPortOpenedUpdate(); + protocolUtil.loadJson(update, data); + return update; + } + + public static fromBuffer(buffer: Buffer) { + const update = new IOPortOpenedUpdate(); + protocolUtil.bufferLoaderHelper(update, buffer, 16, (smartBuffer) => { + protocolUtil.loadCommonUpdateFields(update, smartBuffer, update.data.updateType); + + update.data.port = smartBuffer.readInt32LE(); + }); + return update; + } + + public toBuffer() { + let smartBuffer = new SmartBuffer(); + + smartBuffer.writeInt32LE(this.data.port); // primary_thread_index + + protocolUtil.insertCommonUpdateFields(this, smartBuffer); + return smartBuffer.toBuffer(); + } + + public success = false; + + public readOffset = 0; + + public data = { + /** + * The port number to which the debugging client should connect to read the script's output + */ + port: undefined as number, + + //common props + packetLength: undefined as number, + requestId: 0, //all updates have requestId === 0 + errorCode: ErrorCode.OK, + updateType: UpdateType.IOPortOpened + }; +} + +export function isIOPortOpenedUpdate(event: ProtocolRequest | ProtocolResponse | ProtocolUpdate): event is IOPortOpenedUpdate { + return event?.constructor?.name === IOPortOpenedUpdate.name; +} diff --git a/src/debugProtocol/events/updates/ThreadAttachedUpdate.spec.ts b/src/debugProtocol/events/updates/ThreadAttachedUpdate.spec.ts new file mode 100644 index 00000000..aa5d226f --- /dev/null +++ b/src/debugProtocol/events/updates/ThreadAttachedUpdate.spec.ts @@ -0,0 +1,37 @@ +import { expect } from 'chai'; +import { ErrorCode, StopReason, UpdateType } from '../../Constants'; +import { ThreadAttachedUpdate } from './ThreadAttachedUpdate'; + +describe('AllThreadsStoppedUpdate', () => { + it('serializes and deserializes properly', () => { + const update = ThreadAttachedUpdate.fromJson({ + threadIndex: 1, + stopReason: StopReason.Break, + stopReasonDetail: 'because' + }); + + expect(update.data).to.eql({ + packetLength: undefined, + requestId: 0, + errorCode: ErrorCode.OK, + updateType: UpdateType.ThreadAttached, + + threadIndex: 1, + stopReason: StopReason.Break, + stopReasonDetail: 'because' + }); + + expect( + ThreadAttachedUpdate.fromBuffer(update.toBuffer()).data + ).to.eql({ + packetLength: 29, // 4 bytes + requestId: 0, // 4 bytes + errorCode: ErrorCode.OK, // 4 bytes + updateType: UpdateType.ThreadAttached, // 4 bytes + + threadIndex: 1, // 4 bytes + stopReason: StopReason.Break, // 1 bytes + stopReasonDetail: 'because' // 8 bytes + }); + }); +}); diff --git a/src/debugProtocol/events/updates/ThreadAttachedUpdate.ts b/src/debugProtocol/events/updates/ThreadAttachedUpdate.ts new file mode 100644 index 00000000..9ee84b8a --- /dev/null +++ b/src/debugProtocol/events/updates/ThreadAttachedUpdate.ts @@ -0,0 +1,58 @@ +import { SmartBuffer } from 'smart-buffer'; +import type { StopReason } from '../../Constants'; +import { ErrorCode, StopReasonCode, UpdateType } from '../../Constants'; +import { protocolUtil } from '../../ProtocolUtil'; + +export class ThreadAttachedUpdate { + + public static fromJson(data: { + threadIndex: number; + stopReason: StopReason; + stopReasonDetail: string; + }) { + const update = new ThreadAttachedUpdate(); + protocolUtil.loadJson(update, data); + return update; + } + + public static fromBuffer(buffer: Buffer) { + const update = new ThreadAttachedUpdate(); + protocolUtil.bufferLoaderHelper(update, buffer, 12, (smartBuffer) => { + protocolUtil.loadCommonUpdateFields(update, smartBuffer, update.data.updateType); + update.data.threadIndex = smartBuffer.readInt32LE(); + update.data.stopReason = StopReasonCode[smartBuffer.readUInt8()] as StopReason; + update.data.stopReasonDetail = protocolUtil.readStringNT(smartBuffer); + }); + return update; + } + + public toBuffer() { + const smartBuffer = new SmartBuffer(); + + smartBuffer.writeInt32LE(this.data.threadIndex); + smartBuffer.writeUInt8(StopReasonCode[this.data.stopReason]); + smartBuffer.writeStringNT(this.data.stopReasonDetail); + + protocolUtil.insertCommonUpdateFields(this, smartBuffer); + + return smartBuffer.toBuffer(); + } + + public success = false; + public readOffset = 0; + + public data = { + /** + * The index of the thread that was just attached + */ + threadIndex: undefined as number, + stopReason: undefined as StopReason, + stopReasonDetail: undefined as string, + + //common props + packetLength: undefined as number, + requestId: 0, //all updates have requestId === 0 + errorCode: ErrorCode.OK, + updateType: UpdateType.ThreadAttached + }; +} diff --git a/src/debugProtocol/responses/BreakpointErrorResponse.spec.ts b/src/debugProtocol/responses/BreakpointErrorResponse.spec.ts deleted file mode 100644 index 6bb07222..00000000 --- a/src/debugProtocol/responses/BreakpointErrorResponse.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { createBreakpointErrorUpdateResponse } from './responseCreationHelpers.spec'; -import { expect } from 'chai'; -import { BreakpointErrorUpdateResponse } from './BreakpointErrorUpdateResponse'; -import { ERROR_CODES, UPDATE_TYPES } from '../Constants'; - -describe('BreakpointErrorUpdateResponse', () => { - it('Handles zero errors', () => { - const smartBuffer = createBreakpointErrorUpdateResponse({ - flags: 0, - breakpoint_id: 23, - errorCode: ERROR_CODES.OK, - compile_errors: [], - runtime_errors: [], - other_errors: [], - includePacketLength: false - }); - const rawBuffer = smartBuffer.toBuffer(); - let update = new BreakpointErrorUpdateResponse( - rawBuffer - ); - expect(update.requestId).to.eql(0); - expect(update.errorCode).to.eql(0); - expect(update.updateType).to.eql(UPDATE_TYPES.BREAKPOINT_ERROR); - expect(update.breakpointId).to.eql(23); - expect(update.flags).to.eql(0); - expect(update.success).to.eql(true); - - expect(update.compileErrorCount).to.eql(0); - expect(update.compileErrors).to.eql([]); - - expect(update.runtimeErrorCount).to.eql(0); - expect(update.runtimeErrors).to.eql([]); - - expect(update.otherErrorCount).to.eql(0); - expect(update.otherErrors).to.eql([]); - }); - - it('Handles many errors', () => { - const smartBuffer = createBreakpointErrorUpdateResponse({ - flags: 0, - breakpoint_id: 23, - errorCode: ERROR_CODES.OK, - compile_errors: ['compile error 1'], - runtime_errors: ['runtime error 1', 'runtime error 2'], - other_errors: ['other error 1', 'other error 2', 'other error 3'], - includePacketLength: false - }); - const rawBuffer = smartBuffer.toBuffer(); - let update = new BreakpointErrorUpdateResponse( - rawBuffer - ); - expect(update.compileErrorCount).to.eql(1); - expect(update.compileErrors).to.eql(['compile error 1']); - - expect(update.runtimeErrorCount).to.eql(2); - expect(update.runtimeErrors).to.eql(['runtime error 1', 'runtime error 2']); - - expect(update.otherErrorCount).to.eql(3); - expect(update.otherErrors).to.eql(['other error 1', 'other error 2', 'other error 3']); - }); -}); diff --git a/src/debugProtocol/responses/BreakpointErrorUpdateResponse.ts b/src/debugProtocol/responses/BreakpointErrorUpdateResponse.ts deleted file mode 100644 index f7e14fa3..00000000 --- a/src/debugProtocol/responses/BreakpointErrorUpdateResponse.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { SmartBuffer } from 'smart-buffer'; -import { util } from '../../util'; -import { UPDATE_TYPES } from '../Constants'; - -/** - * Data sent as the data segment of message type: BREAKPOINT_ERROR - ``` - struct BreakpointErrorUpdateData { - uint32 flags; // Always 0, reserved for future use - uint32 breakpoint_id; - uint32 num_compile_errors; - utf8z[num_compile_errors] compile_errors; - uint32 num_runtime_errors; - utf8z[num_runtime_errors] runtime_errors; - uint32 num_other_errors; // E.g., permissions errors - utf8z[num_other_errors] other_errors; - } - ``` -*/ -export class BreakpointErrorUpdateResponse { - - constructor(buffer: Buffer) { - // The minimum size of a undefined response - if (buffer.byteLength >= 12) { - let bufferReader = SmartBuffer.fromBuffer(buffer); - this.requestId = bufferReader.readUInt32LE(); - - // Updates will always have an id of zero because we didn't ask for this information - if (this.requestId === 0) { - this.errorCode = bufferReader.readUInt32LE(); - this.updateType = bufferReader.readUInt32LE(); - } - if (this.updateType === UPDATE_TYPES.BREAKPOINT_ERROR) { - try { - this.flags = bufferReader.readUInt32LE(); // flags - always 0, reserved for future use - this.breakpointId = bufferReader.readUInt32LE(); // breakpoint_id - - this.compileErrorCount = bufferReader.readUInt32LE(); // num_compile_errors - for (let i = 0; i < this.compileErrorCount; i++) { - this.compileErrors.push( - util.readStringNT(bufferReader) - ); - } - - this.runtimeErrorCount = bufferReader.readUInt32LE(); // num_runtime_errors - for (let i = 0; i < this.runtimeErrorCount; i++) { - this.runtimeErrors.push( - util.readStringNT(bufferReader) - ); - } - - this.otherErrorCount = bufferReader.readUInt32LE(); // num_other_errors - for (let i = 0; i < this.otherErrorCount; i++) { - this.otherErrors.push( - util.readStringNT(bufferReader) - ); - } - this.success = true; - } catch (error) { - // Could not process - } - } - } - } - public success = false; - public readOffset = 0; - public requestId = -1; - public errorCode = -1; - public updateType = -1; - - public flags: number; - public breakpointId: number; - - public compileErrorCount: number; - public compileErrors: string[] = []; - - public runtimeErrorCount: number; - public runtimeErrors: string[] = []; - - public otherErrorCount: number; - public otherErrors: string[] = []; -} diff --git a/src/debugProtocol/responses/BreakpointVerifiedUpdateResponse.ts b/src/debugProtocol/responses/BreakpointVerifiedUpdateResponse.ts deleted file mode 100644 index 180c0566..00000000 --- a/src/debugProtocol/responses/BreakpointVerifiedUpdateResponse.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { SmartBuffer } from 'smart-buffer'; -import { util } from '../../util'; -import { UPDATE_TYPES } from '../Constants'; - -/** -``` -// Data sent as the data segment of message type: BREAKPOINT_VERIFIED -struct BreakpointVerifiedUpdateData { - uint32 flags // Always 0, reserved for future use - uint32 num_breakpoints - BreakpointVerifiedInfo[num_breakpoints] breakpoint_verified_info -} - -struct BreakpointVerifiedInfo { - uint32 breakpoint_id -} -``` -*/ -export class BreakpointVerifiedUpdateResponse { - - constructor(buffer: Buffer) { - // The minimum size of a undefined response - if (buffer.byteLength >= 12) { - let bufferReader = SmartBuffer.fromBuffer(buffer); - this.requestId = bufferReader.readUInt32LE(); - - // Updates will always have an id of zero because we didn't ask for this information - if (this.requestId === 0) { - this.errorCode = bufferReader.readUInt32LE(); - this.updateType = bufferReader.readUInt32LE(); - } - if (this.updateType === UPDATE_TYPES.BREAKPOINT_VERIFIED) { - try { - this.flags = bufferReader.readUInt32LE(); // flags - always 0, reserved for future use - - this.numBreakpoints = bufferReader.readUInt32LE(); // num_breakpoints - for (let i = 0; i < this.numBreakpoints; i++) { - this.breakpoints.push({ - breakpointId: bufferReader.readUInt32LE() - }); - } - this.success = true; - } catch (error) { - // Could not process - } - } - } - } - public success = false; - public readOffset = 0; - public requestId = -1; - public errorCode = -1; - public updateType = -1; - - public flags: number; - public numBreakpoints: number; - - public breakpoints: VerifiedBreakpoint[] = []; -} - -export interface VerifiedBreakpoint { - breakpointId: number; -} - -export interface VerifiedBreakpointsData { - breakpoints: VerifiedBreakpoint[]; -} diff --git a/src/debugProtocol/responses/ConnectIOPortResponse.ts b/src/debugProtocol/responses/ConnectIOPortResponse.ts deleted file mode 100644 index e1bbbbe8..00000000 --- a/src/debugProtocol/responses/ConnectIOPortResponse.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { SmartBuffer } from 'smart-buffer'; -import { UPDATE_TYPES } from '../Constants'; - -export class ConnectIOPortResponse { - - constructor(buffer: Buffer) { - // The minimum size of a connect to IO port request - if (buffer.byteLength >= 16) { - try { - let bufferReader = SmartBuffer.fromBuffer(buffer); - this.requestId = bufferReader.readUInt32LE(); // request_id - - // Updates will always have an id of zero because we didn't ask for this information - if (this.requestId === 0) { - this.errorCode = bufferReader.readUInt32LE(); // error_code - this.updateType = bufferReader.readUInt32LE(); // update_type - - // Only handle IO port events in this class - if (this.updateType === UPDATE_TYPES.IO_PORT_OPENED) { - this.data = bufferReader.readUInt32LE(); // data - this.readOffset = bufferReader.readOffset; - this.success = true; - } - } - } catch (error) { - // Could not parse - } - } - } - public success = false; - public readOffset = 0; - - // response fields - public requestId = -1; - public errorCode = -1; - public updateType = -1; - public data = -1; -} diff --git a/src/debugProtocol/responses/ExecuteResponseV3.ts b/src/debugProtocol/responses/ExecuteResponseV3.ts deleted file mode 100644 index aa67cd5a..00000000 --- a/src/debugProtocol/responses/ExecuteResponseV3.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { SmartBuffer } from 'smart-buffer'; -import { util } from '../../util'; - -export class ExecuteResponseV3 { - constructor(buffer: Buffer) { - // The smallest a request response can be - if (buffer.byteLength >= 12) { - try { - let bufferReader = SmartBuffer.fromBuffer(buffer); - this.requestId = bufferReader.readUInt32LE(); // request_id - this.errorCode = bufferReader.readUInt32LE(); // error_code - this.executeSuccess = bufferReader.readUInt8() !== 0; //execute_success - this.runtimeStopCode = bufferReader.readUInt8(); //runtime_stop_code - - this.compileErrors = new ExecuteErrors(bufferReader); - this.runtimeErrors = new ExecuteErrors(bufferReader); - this.otherErrors = new ExecuteErrors(bufferReader); - - this.success = this.compileErrors.success && this.runtimeErrors.success && this.otherErrors.success; - this.readOffset = bufferReader.readOffset; - } catch (error) { - // Could not parse - } - } - } - public success = false; - public readOffset = 0; - /** - * true if code ran and completed without errors, false otherwise - */ - public executeSuccess = false; - public runtimeStopCode: number; - - public compileErrors: ExecuteErrors; - public runtimeErrors: ExecuteErrors; - public otherErrors: ExecuteErrors; - - // response fields - public requestId = -1; - public errorCode = -1; -} - -class ExecuteErrors { - public constructor(bufferReader: SmartBuffer) { - if (bufferReader.length >= 4) { - const errorCount = bufferReader.readUInt32LE(); - for (let i = 0; i < errorCount; i++) { - const message = util.readStringNT(bufferReader); - if (message) { - this.messages.push(message); - } - } - this.success = this.messages.length === errorCount; - } - } - - public success = false; - - public messages: string[] = []; -} diff --git a/src/debugProtocol/responses/HandshakeResponse.spec.ts b/src/debugProtocol/responses/HandshakeResponse.spec.ts deleted file mode 100644 index 03b2bdcc..00000000 --- a/src/debugProtocol/responses/HandshakeResponse.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { HandshakeResponse } from './HandshakeResponse'; -import { Debugger } from '../Debugger'; -import { createHandShakeResponse } from './responseCreationHelpers.spec'; -import { expect } from 'chai'; - -describe('HandshakeResponse', () => { - it('Handles a handshake response', () => { - let mockResponse = createHandShakeResponse({ - magic: Debugger.DEBUGGER_MAGIC, - major: 1, - minor: 0, - patch: 0 - }); - - let handshake = new HandshakeResponse(mockResponse.toBuffer()); - expect(handshake.magic).to.be.equal(Debugger.DEBUGGER_MAGIC); - expect(handshake.majorVersion).to.be.equal(1); - expect(handshake.minorVersion).to.be.equal(0); - expect(handshake.patchVersion).to.be.equal(0); - expect(handshake.readOffset).to.be.equal(mockResponse.writeOffset); - expect(handshake.success).to.be.equal(true); - }); - - it('Fails when buffer is incomplete', () => { - let mockResponse = createHandShakeResponse({ - magic: Debugger.DEBUGGER_MAGIC, - major: 1, - minor: 0, - patch: 0 - }); - - let handshake = new HandshakeResponse(mockResponse.toBuffer().slice(-3)); - expect(handshake.success).to.equal(false); - }); - - it('Fails when the protocol version is equal to or greater then 3.0.0', () => { - let mockResponseV3 = createHandShakeResponse({ - magic: Debugger.DEBUGGER_MAGIC, - major: 3, - minor: 0, - patch: 0 - }); - - let handshakeV3 = new HandshakeResponse(mockResponseV3.toBuffer()); - expect(handshakeV3.success).to.equal(false); - - let mockResponseV301 = createHandShakeResponse({ - magic: Debugger.DEBUGGER_MAGIC, - major: 3, - minor: 0, - patch: 1 - }); - - let handshakeV301 = new HandshakeResponse(mockResponseV301.toBuffer()); - expect(handshakeV301.success).to.equal(false); - }); -}); diff --git a/src/debugProtocol/responses/HandshakeResponse.ts b/src/debugProtocol/responses/HandshakeResponse.ts deleted file mode 100644 index b0fd39bf..00000000 --- a/src/debugProtocol/responses/HandshakeResponse.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { SmartBuffer } from 'smart-buffer'; -import * as semver from 'semver'; -import { util } from '../../util'; - -export class HandshakeResponse { - - constructor(buffer: Buffer) { - // Required size of the handshake - if (buffer.byteLength >= 20) { - try { - let bufferReader = SmartBuffer.fromBuffer(buffer); - this.magic = util.readStringNT(bufferReader); // magic_number - this.majorVersion = bufferReader.readInt32LE(); // protocol_major_version - this.minorVersion = bufferReader.readInt32LE(); // protocol_minor_version - this.patchVersion = bufferReader.readInt32LE(); // protocol_patch_version - this.readOffset = bufferReader.readOffset; - - const versionString = [this.majorVersion, this.minorVersion, this.patchVersion].join('.'); - - // We only support version prior to v3 with this handshake - if (!semver.satisfies(versionString, '<3.0.0')) { - throw new Error(`unsupported version ${versionString}`); - } - this.success = true; - } catch (error) { - // Could not parse - } - } - } - - public watchPacketLength = false; // this will always be false for older protocol versions - public success = false; - public readOffset = 0; - public requestId = 0; - - // response fields - public magic: string; - public majorVersion = -1; - public minorVersion = -1; - public patchVersion = -1; -} diff --git a/src/debugProtocol/responses/HandshakeResponseV3.spec.ts b/src/debugProtocol/responses/HandshakeResponseV3.spec.ts deleted file mode 100644 index 8374a553..00000000 --- a/src/debugProtocol/responses/HandshakeResponseV3.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { HandshakeResponseV3 } from './HandshakeResponseV3'; -import { Debugger } from '../Debugger'; -import { createHandShakeResponseV3 } from './responseCreationHelpers.spec'; -import { expect } from 'chai'; -import { SmartBuffer } from 'smart-buffer'; - -describe('HandshakeResponseV3', () => { - it('Handles a handshake response', () => { - let mockResponse = createHandShakeResponseV3({ - magic: Debugger.DEBUGGER_MAGIC, - major: 3, - minor: 0, - patch: 0, - revisionTimeStamp: Date.now() - }); - - let handshake = new HandshakeResponseV3(mockResponse.toBuffer()); - expect(handshake.magic).to.be.equal(Debugger.DEBUGGER_MAGIC); - expect(handshake.majorVersion).to.be.equal(3); - expect(handshake.minorVersion).to.be.equal(0); - expect(handshake.patchVersion).to.be.equal(0); - expect(handshake.readOffset).to.be.equal(mockResponse.writeOffset); - expect(handshake.success).to.be.equal(true); - }); - - it('Handles a extra packet length in handshake response', () => { - let extraData = new SmartBuffer(); - extraData.writeStringNT('this is extra data'); - extraData.writeUInt32LE(10); - - let mockResponse = createHandShakeResponseV3({ - magic: Debugger.DEBUGGER_MAGIC, - major: 3, - minor: 0, - patch: 0, - revisionTimeStamp: Date.now() - }, extraData.toBuffer()); - - const expectedReadOffset = mockResponse.writeOffset; - - // Write some extra data that the handshake should not include in the readOffSet - mockResponse.writeUInt32LE(123); - - let handshake = new HandshakeResponseV3(mockResponse.toBuffer()); - expect(handshake.magic).to.be.equal(Debugger.DEBUGGER_MAGIC); - expect(handshake.majorVersion).to.be.equal(3); - expect(handshake.minorVersion).to.be.equal(0); - expect(handshake.patchVersion).to.be.equal(0); - expect(handshake.readOffset).to.be.equal(expectedReadOffset); - expect(handshake.success).to.be.equal(true); - }); - - it('Fails when buffer is incomplete', () => { - let mockResponse = createHandShakeResponseV3({ - magic: Debugger.DEBUGGER_MAGIC, - major: 3, - minor: 0, - patch: 0, - revisionTimeStamp: Date.now() - }); - - let handshake = new HandshakeResponseV3(mockResponse.toBuffer().slice(-3)); - expect(handshake.success).to.equal(false); - }); - - it('Fails when the protocol version is less then 3.0.0', () => { - let mockResponseV3 = createHandShakeResponseV3({ - magic: Debugger.DEBUGGER_MAGIC, - major: 2, - minor: 0, - patch: 0, - revisionTimeStamp: Date.now() - }); - - let handshakeV3 = new HandshakeResponseV3(mockResponseV3.toBuffer()); - expect(handshakeV3.success).to.equal(false); - - let mockResponseV301 = createHandShakeResponseV3({ - magic: Debugger.DEBUGGER_MAGIC, - major: 2, - minor: 9, - patch: 9, - revisionTimeStamp: Date.now() - }); - - let handshakeV301 = new HandshakeResponseV3(mockResponseV301.toBuffer()); - expect(handshakeV301.success).to.equal(false); - }); -}); diff --git a/src/debugProtocol/responses/HandshakeResponseV3.ts b/src/debugProtocol/responses/HandshakeResponseV3.ts deleted file mode 100644 index 4c51cc88..00000000 --- a/src/debugProtocol/responses/HandshakeResponseV3.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { SmartBuffer } from 'smart-buffer'; -import * as semver from 'semver'; -import { util } from '../../util'; - -export class HandshakeResponseV3 { - - constructor(buffer: Buffer) { - // Required size of the handshake - if (buffer.byteLength >= 20) { - try { - let bufferReader = SmartBuffer.fromBuffer(buffer); - this.magic = util.readStringNT(bufferReader); // magic_number - this.majorVersion = bufferReader.readInt32LE(); // protocol_major_version - this.minorVersion = bufferReader.readInt32LE(); // protocol_minor_version - this.patchVersion = bufferReader.readInt32LE(); // protocol_patch_version - - const legacyReadSize = bufferReader.readOffset; - this.remainingPacketLength = bufferReader.readInt32LE(); // remaining_packet_length - - const requiredBufferSize = this.remainingPacketLength + legacyReadSize; - this.revisionTimeStamp = new Date(Number(bufferReader.readBigUInt64LE())); // platform_revision_timestamp - - if (bufferReader.length < requiredBufferSize) { - throw new Error(`Missing buffer data according to the remaining packet length: ${bufferReader.length}/${requiredBufferSize}`); - } - this.readOffset = requiredBufferSize; - - const versionString = [this.majorVersion, this.minorVersion, this.patchVersion].join('.'); - - // We only support v3 or above with this handshake - if (!semver.satisfies(versionString, '>=3.0.0')) { - throw new Error(`unsupported version ${versionString}`); - } - this.success = true; - } catch (error) { - // Could not parse - } - } - } - - public watchPacketLength = true; // this will always be false for the new protocol versions - public success = false; - public readOffset = 0; - public requestId = 0; - - // response fields - public magic: string; - public majorVersion = -1; - public minorVersion = -1; - public patchVersion = -1; - public remainingPacketLength = -1; - public revisionTimeStamp: Date; -} diff --git a/src/debugProtocol/responses/ListBreakpointsResponse.spec.ts b/src/debugProtocol/responses/ListBreakpointsResponse.spec.ts deleted file mode 100644 index 1f756ab6..00000000 --- a/src/debugProtocol/responses/ListBreakpointsResponse.spec.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { createListBreakpointsResponse, getRandomBuffer } from './responseCreationHelpers.spec'; -import { expect } from 'chai'; -import { ListBreakpointsResponse } from './ListBreakpointsResponse'; -import { ERROR_CODES } from '../Constants'; - -describe('ListBreakpointsResponse', () => { - let response: ListBreakpointsResponse; - beforeEach(() => { - response = undefined; - }); - it('handles empty buffer', () => { - response = new ListBreakpointsResponse(null); - //Great, it didn't explode! - expect(response.success).to.be.false; - }); - - it('handles undersized buffers', () => { - response = new ListBreakpointsResponse( - getRandomBuffer(0) - ); - expect(response.success).to.be.false; - - response = new ListBreakpointsResponse( - getRandomBuffer(1) - ); - expect(response.success).to.be.false; - - response = new ListBreakpointsResponse( - getRandomBuffer(11) - ); - expect(response.success).to.be.false; - }); - - it('gracefully handles mismatched breakpoint count', () => { - const bp1 = { - breakpointId: 1, - errorCode: ERROR_CODES.OK, - hitCount: 0, - success: true - }; - response = new ListBreakpointsResponse( - createListBreakpointsResponse({ - requestId: 1, - num_breakpoints: 2, - breakpoints: [bp1] - }).toBuffer() - ); - expect(response.success).to.be.false; - expect(response.breakpoints).to.eql([bp1]); - }); - - it('handles malformed breakpoint data', () => { - const bp1 = { - breakpointId: 1, - errorCode: ERROR_CODES.OK, - hitCount: 2, - success: true - }; - response = new ListBreakpointsResponse( - createListBreakpointsResponse({ - requestId: 1, - num_breakpoints: 2, - breakpoints: [ - bp1, - { - //missing all other bp properties - breakpointId: 1 - } - ] - }).toBuffer() - ); - expect(response.success).to.be.false; - expect(response.breakpoints).to.eql([bp1]); - }); - - it('handles malformed breakpoint data', () => { - const bp1 = { - breakpointId: 0, - errorCode: ERROR_CODES.OK, - success: true - }; - response = new ListBreakpointsResponse( - createListBreakpointsResponse({ - requestId: 1, - num_breakpoints: 2, - breakpoints: [bp1] - }).toBuffer() - ); - expect(response.success).to.be.false; - //hitcount should not be set when bpId is zero - expect(response.breakpoints[0].hitCount).to.be.undefined; - //the breakpoint should not be verified if bpId === 0 - expect(response.breakpoints[0].isVerified).to.be.false; - }); - - it('reads breakpoint data properly', () => { - const bp1 = { - breakpointId: 1, - errorCode: ERROR_CODES.OK, - hitCount: 0, - success: true - }; - response = new ListBreakpointsResponse( - createListBreakpointsResponse({ - requestId: 1, - breakpoints: [bp1] - }).toBuffer() - ); - expect(response.success).to.be.true; - expect(response.breakpoints).to.eql([bp1]); - expect(response.breakpoints[0].isVerified).to.be.true; - }); - - it('reads breakpoint data properly', () => { - const bp1 = { - breakpointId: 1, - errorCode: ERROR_CODES.NOT_STOPPED, - hitCount: 0, - success: true - }; - response = new ListBreakpointsResponse( - createListBreakpointsResponse({ - requestId: 1, - breakpoints: [bp1] - }).toBuffer() - ); - expect( - response.breakpoints[0].errorText - ).to.eql( - ERROR_CODES[ERROR_CODES.NOT_STOPPED] - ); - }); -}); diff --git a/src/debugProtocol/responses/ListBreakpointsResponse.ts b/src/debugProtocol/responses/ListBreakpointsResponse.ts deleted file mode 100644 index ca17cd3c..00000000 --- a/src/debugProtocol/responses/ListBreakpointsResponse.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { SmartBuffer } from 'smart-buffer'; -import { ERROR_CODES } from '../Constants'; - -export class ListBreakpointsResponse { - - constructor(buffer: Buffer) { - // The minimum size of a request - if (buffer?.byteLength >= 12) { - try { - let bufferReader = SmartBuffer.fromBuffer(buffer); - this.requestId = bufferReader.readUInt32LE(); // request_id - - // Any request id less then one is an update and we should not process it here - if (this.requestId > 0) { - this.errorCode = bufferReader.readUInt32LE(); - this.numBreakpoints = bufferReader.readUInt32LE(); // num_breakpoints - The number of breakpoints in the breakpoints array. - - // build the list of BreakpointInfo - for (let i = 0; i < this.numBreakpoints; i++) { - let breakpointInfo = new BreakpointInfo(bufferReader); - // All the necessary data was present, so keep this item - this.breakpoints.push(breakpointInfo); - } - - this.readOffset = bufferReader.readOffset; - this.success = (this.breakpoints.length === this.numBreakpoints); - } - } catch (error) { - // Could not parse - } - } - } - public success = false; - public readOffset = 0; - - // response fields - public requestId = -1; - public numBreakpoints: number; - public breakpoints = [] as BreakpointInfo[]; - public data = -1; - public errorCode: ERROR_CODES; -} - -export class BreakpointInfo { - constructor(bufferReader: SmartBuffer) { - // breakpoint_id - The ID assigned to the breakpoint. An ID greater than 0 indicates an active breakpoint. An ID of 0 denotes that the breakpoint has an error. - this.breakpointId = bufferReader.readUInt32LE(); - // error_code - Indicates whether the breakpoint was successfully returned. - this.errorCode = bufferReader.readUInt32LE(); - - if (this.breakpointId > 0) { - // This argument is only present if the breakpoint_id is valid. - // ignore_count - Current state, decreases as breakpoint is executed. - this.hitCount = bufferReader.readUInt32LE(); - } - this.success = true; - } - - public get isVerified() { - return this.breakpointId > 0; - } - public success = false; - public breakpointId: number; - public errorCode: number; - /** - * The textual description of the error code - */ - public get errorText() { - return ERROR_CODES[this.errorCode]; - } - public hitCount: number; -} diff --git a/src/debugProtocol/responses/ProtocolEvent.spec.ts b/src/debugProtocol/responses/ProtocolEvent.spec.ts deleted file mode 100644 index c22bd317..00000000 --- a/src/debugProtocol/responses/ProtocolEvent.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ProtocolEvent } from './ProtocolEvent'; -import { createHandShakeResponse, createProtocolEvent } from './responseCreationHelpers.spec'; -import { Debugger } from '../Debugger'; -import { expect } from 'chai'; -import { ERROR_CODES, UPDATE_TYPES } from '../Constants'; - -describe('ProtocolEvent', () => { - it('Handles a Protocol update events', () => { - let mockResponse = createProtocolEvent({ - requestId: 0, - errorCode: ERROR_CODES.CANT_CONTINUE, - updateType: UPDATE_TYPES.ALL_THREADS_STOPPED - }); - - let protocolEvent = new ProtocolEvent(mockResponse.toBuffer()); - expect(protocolEvent.requestId).to.be.equal(0); - expect(protocolEvent.errorCode).to.be.equal(ERROR_CODES.CANT_CONTINUE); - expect(protocolEvent.updateType).to.be.equal(UPDATE_TYPES.ALL_THREADS_STOPPED); - expect(protocolEvent.readOffset).to.be.equal(mockResponse.writeOffset); - expect(protocolEvent.success).to.be.equal(true); - }); - - it('Handles a Protocol response events', () => { - let mockResponse = createProtocolEvent({ - requestId: 1, - errorCode: ERROR_CODES.OK - }); - - let protocolEvent = new ProtocolEvent(mockResponse.toBuffer()); - expect(protocolEvent.requestId).to.be.equal(1); - expect(protocolEvent.errorCode).to.be.equal(ERROR_CODES.OK); - expect(protocolEvent.updateType).to.be.equal(-1); - expect(protocolEvent.readOffset).to.be.equal(mockResponse.writeOffset); - expect(protocolEvent.success).to.be.equal(true); - }); - - it('Fails when buffer is incomplete', () => { - let mockResponse = createHandShakeResponse({ - magic: Debugger.DEBUGGER_MAGIC, - major: 1, - minor: 0, - patch: 0 - }); - - let protocolEvent = new ProtocolEvent(mockResponse.toBuffer().slice(-3)); - expect(protocolEvent.success).to.equal(false); - }); -}); diff --git a/src/debugProtocol/responses/ProtocolEvent.ts b/src/debugProtocol/responses/ProtocolEvent.ts deleted file mode 100644 index cf77b290..00000000 --- a/src/debugProtocol/responses/ProtocolEvent.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { SmartBuffer } from 'smart-buffer'; - -export class ProtocolEvent { - constructor(buffer: Buffer) { - // The smallest a request response can be - if (buffer.byteLength >= 8) { - try { - let bufferReader = SmartBuffer.fromBuffer(buffer); - this.requestId = bufferReader.readUInt32LE(); // request_id - this.errorCode = bufferReader.readUInt32LE(); // error_code - - // Any request id less then one is an update and we should not process it here - if (this.requestId > 0) { - this.readOffset = bufferReader.readOffset; - } else if (this.requestId === 0) { - this.updateType = bufferReader.readUInt32LE(); - } - this.readOffset = bufferReader.readOffset; - this.success = true; - } catch (error) { - // Could not parse - } - } - } - - public success = false; - public readOffset = 0; - - // response fields - public packetLength = 0; - public requestId = -1; - public updateType = -1; - public errorCode = -1; - public data = -1; -} diff --git a/src/debugProtocol/responses/ProtocolEventV3.ts b/src/debugProtocol/responses/ProtocolEventV3.ts deleted file mode 100644 index a624d9a4..00000000 --- a/src/debugProtocol/responses/ProtocolEventV3.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { SmartBuffer } from 'smart-buffer'; - -export class ProtocolEventV3 { - constructor(buffer: Buffer) { - // The smallest a request response can be - if (buffer.byteLength >= 12) { - try { - let bufferReader = SmartBuffer.fromBuffer(buffer); - this.packetLength = bufferReader.readUInt32LE(); // packet_length - this.requestId = bufferReader.readUInt32LE(); // request_id - this.errorCode = bufferReader.readUInt32LE(); // error_code - - if (bufferReader.length < this.packetLength) { - throw new Error(`Incomplete packet. Bytes received: ${bufferReader.length}/${this.packetLength}`); - } - - // Any request id less then one is an update and we should not process it here - if (this.requestId > 0) { - this.readOffset = bufferReader.readOffset; - } else if (this.requestId === 0) { - this.updateType = bufferReader.readUInt32LE(); - } - this.readOffset = bufferReader.readOffset; - this.success = true; - } catch (error) { - // Could not parse - } - } - } - public success = false; - public readOffset = 0; - - // response fields - public packetLength = 0; - public requestId = -1; - public updateType = -1; - public errorCode = -1; - public data = -1; -} diff --git a/src/debugProtocol/responses/StackTraceResponse.ts b/src/debugProtocol/responses/StackTraceResponse.ts deleted file mode 100644 index ce2caf95..00000000 --- a/src/debugProtocol/responses/StackTraceResponse.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as path from 'path'; -import { SmartBuffer } from 'smart-buffer'; -import { util } from '../../util'; - -export class StackTraceResponse { - - constructor(buffer: Buffer) { - // The smallest a stacktrace request response can be - if (buffer.byteLength >= 8) { - try { - let bufferReader = SmartBuffer.fromBuffer(buffer); - this.requestId = bufferReader.readUInt32LE(); - - // Any request id less then one is an update and we should not process it here - if (this.requestId > 0) { - this.errorCode = bufferReader.readUInt32LE(); - this.stackSize = bufferReader.readUInt32LE(); - - for (let i = 0; i < this.stackSize; i++) { - let stackEntry = new StackEntry(bufferReader); - if (stackEntry.success) { - // All the necessary stack entry data was present. Push to the entries array. - this.entries.push(stackEntry); - } - } - - this.readOffset = bufferReader.readOffset; - this.success = (this.entries.length === this.stackSize); - } - } catch (error) { - // Could not parse - } - } - } - public success = false; - public readOffset = 0; - - // response fields - public requestId = -1; - public errorCode = -1; - public stackSize = -1; - public entries = []; -} - -export class StackEntry { - - constructor(bufferReader: SmartBuffer) { - this.lineNumber = bufferReader.readUInt32LE(); - // NOTE: this is documented as being function name then file name but it is being returned by the device backwards. - this.fileName = util.readStringNT(bufferReader); - this.functionName = util.readStringNT(bufferReader); - - let fileExtension = path.extname(this.fileName).toLowerCase(); - // NOTE:Make sure we have a full valid path (?? can be valid because the device might not know the file). - this.success = (fileExtension === '.brs' || fileExtension === '.xml' || this.fileName === '??'); - } - public success = false; - - // response fields - public lineNumber = -1; - public functionName: string; - public fileName: string; -} diff --git a/src/debugProtocol/responses/StackTraceResponseV3.ts b/src/debugProtocol/responses/StackTraceResponseV3.ts deleted file mode 100644 index 55c9289d..00000000 --- a/src/debugProtocol/responses/StackTraceResponseV3.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as path from 'path'; -import { SmartBuffer } from 'smart-buffer'; -import { util } from '../../util'; - -export class StackTraceResponseV3 { - - constructor(buffer: Buffer) { - // The smallest a stacktrace request response can be - if (buffer.byteLength >= 8) { - try { - let bufferReader = SmartBuffer.fromBuffer(buffer); - this.requestId = bufferReader.readUInt32LE(); - - // Any request id less then one is an update and we should not process it here - if (this.requestId > 0) { - this.errorCode = bufferReader.readUInt32LE(); - this.stackSize = bufferReader.readUInt32LE(); - - for (let i = 0; i < this.stackSize; i++) { - let stackEntry = new StackEntryV3(bufferReader); - if (stackEntry.success) { - // All the necessary stack entry data was present. Push to the entries array. - this.entries.push(stackEntry); - } - } - - this.readOffset = bufferReader.readOffset; - this.success = (this.entries.length === this.stackSize); - } - } catch (error) { - // Could not parse - } - } - } - public success = false; - public readOffset = 0; - - // response fields - public requestId = -1; - public errorCode = -1; - public stackSize = -1; - public entries = []; -} - -export class StackEntryV3 { - - constructor(bufferReader: SmartBuffer) { - this.lineNumber = bufferReader.readUInt32LE(); - // NOTE: this is documented as being function name then file name but it is being returned by the device backwards. - this.functionName = util.readStringNT(bufferReader); - this.fileName = util.readStringNT(bufferReader); - - let fileExtension = path.extname(this.fileName).toLowerCase(); - // NOTE:Make sure we have a full valid path (?? can be valid because the device might not know the file). - this.success = (fileExtension === '.brs' || fileExtension === '.xml' || this.fileName === '??'); - } - public success = false; - - // response fields - public lineNumber = -1; - public functionName: string; - public fileName: string; -} diff --git a/src/debugProtocol/responses/ThreadsResponse.ts b/src/debugProtocol/responses/ThreadsResponse.ts deleted file mode 100644 index de96943c..00000000 --- a/src/debugProtocol/responses/ThreadsResponse.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as path from 'path'; -import { SmartBuffer } from 'smart-buffer'; -import { STOP_REASONS } from '../Constants'; -import { util } from '../../util'; - -export class ThreadsResponse { - - constructor(buffer: Buffer) { - // The smallest a threads request response can be - if (buffer.byteLength >= 21) { - try { - let bufferReader = SmartBuffer.fromBuffer(buffer); - this.requestId = bufferReader.readUInt32LE(); - - // Any request id less then one is an update and we should not process it here - if (this.requestId > 0) { - this.errorCode = bufferReader.readUInt32LE(); - this.threadsCount = bufferReader.readUInt32LE(); - - for (let i = 0; i < this.threadsCount; i++) { - let stackEntry = new ThreadInfo(bufferReader); - if (stackEntry.success) { - // All the necessary stack entry data was present. Push to the entries array. - this.threads.push(stackEntry); - } - } - - this.readOffset = bufferReader.readOffset; - this.success = (this.threads.length === this.threadsCount); - } - } catch (error) { - // Could not parse - } - } - } - public success = false; - public readOffset = 0; - - // response fields - public requestId = -1; - public errorCode = -1; - public threadsCount = -1; - public threads = []; -} - -export class ThreadInfo { - - constructor(bufferReader: SmartBuffer) { - // NOTE: The docs say the flags should be unit8 and uint32. In testing it seems like they are sending uint32 but meant to send unit8. - // eslint-disable-next-line no-bitwise - this.isPrimary = (bufferReader.readUInt32LE() & 0x01) > 0; - this.stopReason = STOP_REASONS[bufferReader.readUInt8()]; - this.stopReasonDetail = util.readStringNT(bufferReader); - this.lineNumber = bufferReader.readUInt32LE(); - this.functionName = util.readStringNT(bufferReader); - this.fileName = util.readStringNT(bufferReader); - this.codeSnippet = util.readStringNT(bufferReader); - - let fileExtension = path.extname(this.fileName).toLowerCase(); - // NOTE: Make sure we have a full valid path (?? can be valid because the device might not know the file) and that we have a codeSnippet. - this.success = (fileExtension === '.brs' || fileExtension === '.xml' || this.fileName === '??') && this.codeSnippet.length > 1; - } - public success = false; - - // response fields - public isPrimary: boolean; - public stopReason: string; - public stopReasonDetail: string; - public lineNumber = -1; - public functionName: string; - public fileName: string; - public codeSnippet: string; -} diff --git a/src/debugProtocol/responses/UndefinedResponse.ts b/src/debugProtocol/responses/UndefinedResponse.ts deleted file mode 100644 index 6dfae2aa..00000000 --- a/src/debugProtocol/responses/UndefinedResponse.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { SmartBuffer } from 'smart-buffer'; -import { UPDATE_TYPES } from '../Constants'; - -export class UndefinedResponse { - - constructor(buffer: Buffer) { - // The minimum size of a undefined response - if (buffer.byteLength >= 12) { - try { - let bufferReader = SmartBuffer.fromBuffer(buffer); - this.requestId = bufferReader.readUInt32LE(); - - // Updates will always have an id of zero because we didn't ask for this information - if (this.requestId === 0) { - this.errorCode = bufferReader.readUInt32LE(); - this.updateType = bufferReader.readUInt32LE(); - - // Only handle undefined events in this class - if (this.updateType === UPDATE_TYPES.UNDEF) { - this.data = bufferReader.readUInt8(); - this.readOffset = bufferReader.readOffset; - this.success = true; - } - } - } catch (error) { - // Could not process - } - } - } - public success = false; - public readOffset = 0; - - // response fields - public requestId = -1; - public errorCode = -1; - public updateType = -1; - public data = -1; -} diff --git a/src/debugProtocol/responses/UpdateThreadsResponse.ts b/src/debugProtocol/responses/UpdateThreadsResponse.ts deleted file mode 100644 index ac0e7741..00000000 --- a/src/debugProtocol/responses/UpdateThreadsResponse.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { SmartBuffer } from 'smart-buffer'; -import { STOP_REASONS, UPDATE_TYPES } from '../Constants'; -import { util } from '../../util'; - -export class UpdateThreadsResponse { - - constructor(buffer: Buffer) { - if (buffer.byteLength >= 12) { - try { - let bufferReader = SmartBuffer.fromBuffer(buffer); - this.requestId = bufferReader.readUInt32LE(); - if (this.requestId === 0) { - this.errorCode = bufferReader.readUInt32LE(); - this.updateType = bufferReader.readUInt32LE(); - - let threadsUpdate: ThreadAttached | ThreadsStopped; - if (this.updateType === UPDATE_TYPES.ALL_THREADS_STOPPED) { - threadsUpdate = new ThreadsStopped(bufferReader); - } else if (this.updateType === UPDATE_TYPES.THREAD_ATTACHED) { - threadsUpdate = new ThreadAttached(bufferReader); - } - - if (threadsUpdate?.success) { - this.data = threadsUpdate; - this.readOffset = bufferReader.readOffset; - this.success = true; - } - } - } catch (error) { - // Can't be parsed - } - } - } - public success = false; - public readOffset = 0; - - // response fields - public requestId = -1; - public errorCode = -1; - public updateType = -1; - public data: ThreadAttached | ThreadsStopped; -} - -export class ThreadsStopped { - - constructor(bufferReader: SmartBuffer) { - if (bufferReader.length >= bufferReader.readOffset + 6) { - this.primaryThreadIndex = bufferReader.readInt32LE(); - this.stopReason = getStopReason(bufferReader.readUInt8()); - this.stopReasonDetail = util.readStringNT(bufferReader); - this.success = true; - } - } - public success = false; - - // response fields - public primaryThreadIndex = -1; - public stopReason = -1; - public stopReasonDetail: string; -} - -export class ThreadAttached { - - constructor(bufferReader: SmartBuffer) { - if (bufferReader.length >= bufferReader.readOffset + 6) { - this.threadIndex = bufferReader.readInt32LE(); - this.stopReason = getStopReason(bufferReader.readUInt8()); - this.stopReasonDetail = util.readStringNT(bufferReader); - this.success = true; - } - } - public success = false; - - // response fields - public threadIndex = -1; - public stopReason = -1; - public stopReasonDetail: string; -} - -function getStopReason(value: number): STOP_REASONS { - switch (value) { - case STOP_REASONS.BREAK: - return STOP_REASONS.BREAK; - case STOP_REASONS.NORMAL_EXIT: - return STOP_REASONS.NORMAL_EXIT; - case STOP_REASONS.NOT_STOPPED: - return STOP_REASONS.NOT_STOPPED; - case STOP_REASONS.RUNTIME_ERROR: - return STOP_REASONS.RUNTIME_ERROR; - case STOP_REASONS.STOP_STATEMENT: - return STOP_REASONS.STOP_STATEMENT; - case STOP_REASONS.UNDEFINED: - return STOP_REASONS.UNDEFINED; - default: - return STOP_REASONS.UNDEFINED; - } -} diff --git a/src/debugProtocol/responses/VariableResponse.spec.ts b/src/debugProtocol/responses/VariableResponse.spec.ts deleted file mode 100644 index b950d9bd..00000000 --- a/src/debugProtocol/responses/VariableResponse.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* eslint-disable no-bitwise */ -import { VariableResponse } from './VariableResponse'; -import { createVariableResponse } from './responseCreationHelpers.spec'; -import { expect } from 'chai'; -import { ERROR_CODES, VARIABLE_FLAGS, VARIABLE_TYPES } from '../Constants'; - -describe('VariableResponse', () => { - - it('Properly parses invalid variable', () => { - let buffer = createVariableResponse({ - requestId: 2, - errorCode: ERROR_CODES.OK, - variables: [{ - name: 'person', - refCount: 2, - isConst: false, - variableType: VARIABLE_TYPES.AA, - keyType: VARIABLE_TYPES.String, - value: undefined, - children: [{ - name: 'firstName', - refCount: 1, - value: 'Bob', - variableType: VARIABLE_TYPES.String, - isConst: false - }, { - name: 'lastName', - refCount: 1, - value: undefined, - variableType: VARIABLE_TYPES.Invalid, - isConst: false - }] - }], - includePacketLength: false - }); - - let response = new VariableResponse(buffer.toBuffer()); - expect( - response.variables?.map(x => ({ - name: x.name, - value: x.value, - isContainer: x.isContainer - })) - ).to.eql([{ - name: 'person', - value: null, - isContainer: true - }, { - name: 'firstName', - value: 'Bob', - isContainer: false - }, { - name: 'lastName', - value: 'Invalid', - isContainer: false - }]); - }); -}); diff --git a/src/debugProtocol/responses/VariableResponse.ts b/src/debugProtocol/responses/VariableResponse.ts deleted file mode 100644 index fd268e03..00000000 --- a/src/debugProtocol/responses/VariableResponse.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* eslint-disable no-bitwise */ -import { SmartBuffer } from 'smart-buffer'; -import { VARIABLE_FLAGS, VARIABLE_TYPES } from '../Constants'; -import { util } from '../../util'; - -export class VariableResponse { - - constructor(buffer: Buffer) { - // Minimum variable request response size - if (buffer.byteLength >= 13) { - try { - let bufferReader = SmartBuffer.fromBuffer(buffer); - this.requestId = bufferReader.readUInt32LE(); - - // Any request id less then one is an update and we should not process it here - if (this.requestId > 0) { - this.errorCode = bufferReader.readUInt32LE(); - this.numVariables = bufferReader.readUInt32LE(); - - // iterate over each variable in the buffer data and create a Variable Info object - for (let i = 0; i < this.numVariables; i++) { - let variableInfo = new VariableInfo(bufferReader); - if (variableInfo.success) { - // All the necessary variable data was present. Push to the variables array. - this.variables.push(variableInfo); - } - } - - this.readOffset = bufferReader.readOffset; - this.success = (this.variables.length === this.numVariables); - } - } catch (error) { - // Could not process - } - } - } - public success = false; - public readOffset = 0; - - // response fields - public requestId = -1; - public errorCode = -1; - public numVariables = -1; - public variables = [] as VariableInfo[]; -} - -export class VariableInfo { - - constructor(bufferReader: SmartBuffer) { - if (bufferReader.length >= 13) { - let bitwiseMask = bufferReader.readUInt8(); - - // Determine the different variable properties - this.isChildKey = (bitwiseMask & VARIABLE_FLAGS.isChildKey) > 0; - this.isConst = (bitwiseMask & VARIABLE_FLAGS.isConst) > 0; - this.isContainer = (bitwiseMask & VARIABLE_FLAGS.isContainer) > 0; - this.isNameHere = (bitwiseMask & VARIABLE_FLAGS.isNameHere) > 0; - this.isRefCounted = (bitwiseMask & VARIABLE_FLAGS.isRefCounted) > 0; - this.isValueHere = (bitwiseMask & VARIABLE_FLAGS.isValueHere) > 0; - - this.variableType = VARIABLE_TYPES[bufferReader.readUInt8()]; - - if (this.isNameHere) { - // YAY we have a name. Pull it out of the buffer. - this.name = util.readStringNT(bufferReader); - } - - if (this.isRefCounted) { - // This variables reference counts are tracked and we can pull it from the buffer. - this.refCount = bufferReader.readUInt32LE(); - } - - if (this.isContainer) { - // It is a form of container object. - // Are the key strings or integers for example - this.keyType = VARIABLE_TYPES[bufferReader.readUInt8()]; - // Equivalent to length on arrays - this.elementCount = bufferReader.readUInt32LE(); - } - - // Pull out the variable data based on the type if that type returns a value - switch (this.variableType) { - case 'Interface': - case 'Object': - case 'String': - case 'Subroutine': - case 'Function': - this.value = util.readStringNT(bufferReader); - this.success = true; - break; - case 'Subtyped_Object': - let names = []; - for (let i = 0; i < 2; i++) { - names.push(util.readStringNT(bufferReader)); - } - - if (names.length === 2) { - this.value = names.join('; '); - this.success = true; - } - break; - case 'Boolean': - this.value = (bufferReader.readUInt8() > 0); - this.success = true; - break; - case 'Double': - this.value = bufferReader.readDoubleLE(); - this.success = true; - break; - case 'Float': - this.value = bufferReader.readFloatLE(); - this.success = true; - break; - case 'Integer': - this.value = bufferReader.readInt32LE(); - this.success = true; - break; - case 'LongInteger': - this.value = bufferReader.readBigInt64LE(); - this.success = true; - break; - case 'Uninitialized': - this.value = 'Uninitialized'; - this.success = true; - break; - case 'Unknown': - this.value = 'Unknown'; - this.success = true; - break; - case 'Invalid': - this.value = 'Invalid'; - this.success = true; - break; - case 'AA': - case 'Array': - case 'List': - this.value = null; - this.success = true; - break; - default: - this.value = null; - this.success = false; - } - } - } - public success = false; - - // response flags - public isChildKey: boolean; - public isConst: boolean; - public isContainer: boolean; - public isNameHere: boolean; - public isRefCounted: boolean; - public isValueHere: boolean; - - // response fields - public variableType: string; - public name: string; - public refCount = -1; - public keyType: string; - public elementCount = -1; - public value: number | string | boolean | bigint | null; -} diff --git a/src/debugProtocol/responses/index.ts b/src/debugProtocol/responses/index.ts deleted file mode 100644 index 121a7eab..00000000 --- a/src/debugProtocol/responses/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from './ConnectIOPortResponse'; -export * from './HandshakeResponse'; -export * from './HandshakeResponseV3'; -export * from './ProtocolEvent'; -export * from './ProtocolEventV3'; -export * from './StackTraceResponse'; -export * from './StackTraceResponseV3'; -export * from './ThreadsResponse'; -export * from './UndefinedResponse'; -export * from './UpdateThreadsResponse'; -export * from './VariableResponse'; diff --git a/src/debugProtocol/responses/responseCreationHelpers.spec.ts b/src/debugProtocol/responses/responseCreationHelpers.spec.ts deleted file mode 100644 index 452bf756..00000000 --- a/src/debugProtocol/responses/responseCreationHelpers.spec.ts +++ /dev/null @@ -1,290 +0,0 @@ -/* eslint-disable @typescript-eslint/no-loop-func */ -/* eslint-disable no-bitwise */ -import { SmartBuffer } from 'smart-buffer'; -import { ERROR_CODES, UPDATE_TYPES, VARIABLE_FLAGS, VARIABLE_TYPES } from '../Constants'; -import type { BreakpointInfo } from './ListBreakpointsResponse'; - -interface Handshake { - magic: string; - major: number; - minor: number; - patch: number; -} - -export function createHandShakeResponse(handshake: Handshake): SmartBuffer { - let buffer = new SmartBuffer(); - buffer.writeStringNT(handshake.magic); // magic_number - buffer.writeUInt32LE(handshake.major); // protocol_major_version - buffer.writeUInt32LE(handshake.minor); // protocol_minor_version - buffer.writeUInt32LE(handshake.patch); // protocol_patch_version - return buffer; -} - -interface HandshakeV3 { - magic: string; - major: number; - minor: number; - patch: number; - - // populated by helper. - // commented out here for the sake of documenting it. - // remainingPacketLength: number; - - revisionTimeStamp: number; -} - -export function createHandShakeResponseV3(handshake: HandshakeV3, extraBufferData?: Buffer): SmartBuffer { - let buffer = new SmartBuffer(); - buffer.writeStringNT(handshake.magic); // magic_number - buffer.writeUInt32LE(handshake.major); // protocol_major_version - buffer.writeUInt32LE(handshake.minor); // protocol_minor_version - buffer.writeUInt32LE(handshake.patch); // protocol_patch_version - - let timeStampBuffer = new SmartBuffer(); - timeStampBuffer.writeBigInt64LE(BigInt(handshake.revisionTimeStamp)); // platform_revision_timestamp - - buffer.writeUInt32LE(timeStampBuffer.writeOffset + 4 + (extraBufferData ? extraBufferData.length : 0)); // remaining_packet_length - buffer.writeBuffer(timeStampBuffer.toBuffer()); - - if (extraBufferData) { - buffer.writeBuffer(extraBufferData); - } - - return buffer; -} - -interface ProtocolEvent { - requestId: number; - errorCode: ERROR_CODES; - updateType?: UPDATE_TYPES; -} - -export function createProtocolEvent(protocolEvent: ProtocolEvent, extraBufferData?: Buffer): SmartBuffer { - let buffer = new SmartBuffer(); - buffer.writeUInt32LE(protocolEvent.requestId); // request_id - buffer.writeUInt32LE(protocolEvent.errorCode); // error_code - - // If this is an update type make sure to add the update type value - if (protocolEvent.requestId === 0) { - buffer.writeInt32LE(protocolEvent.updateType); // update_type - } - - // write any extra data for testing - if (extraBufferData) { - buffer.writeBuffer(extraBufferData); - } - - return buffer; -} - -export function createProtocolEventV3(protocolEvent: ProtocolEvent, extraBufferData?: Buffer): SmartBuffer { - let buffer = new SmartBuffer(); - buffer.writeUInt32LE(protocolEvent.requestId); // request_id - buffer.writeUInt32LE(protocolEvent.errorCode); // error_code - - // If this is an update type make sure to add the update type value - if (protocolEvent.requestId === 0) { - buffer.writeInt32LE(protocolEvent.updateType); // update_type - } - - // write any extra data for testing - if (extraBufferData) { - buffer.writeBuffer(extraBufferData); - } - - return addPacketLength(buffer); -} - -/** - * Add packetLength to the beginning of the buffer - */ -function addPacketLength(buffer: SmartBuffer): SmartBuffer { - return buffer.insertUInt32LE(buffer.length + 4, 0); // packet_length - The size of the packet to be sent. -} - -/** - * Create a buffer for `ListBreakpointsResponse` - */ -export function createListBreakpointsResponse(params: { requestId?: number; errorCode?: number; num_breakpoints?: number; breakpoints?: Partial[]; extraBufferData?: Buffer }): SmartBuffer { - let buffer = new SmartBuffer(); - - writeIfSet(params.requestId, x => buffer.writeUInt32LE(x)); - writeIfSet(params.errorCode, x => buffer.writeUInt32LE(x)); - - buffer.writeUInt32LE(params.num_breakpoints ?? params.breakpoints?.length ?? 0); // num_breakpoints - for (const breakpoint of params?.breakpoints ?? []) { - writeIfSet(breakpoint.breakpointId, x => buffer.writeUInt32LE(x)); - writeIfSet(breakpoint.errorCode, x => buffer.writeUInt32LE(x)); - writeIfSet(breakpoint.hitCount, x => buffer.writeUInt32LE(x)); - } - - // write any extra data for testing - writeIfSet(params.extraBufferData, x => buffer.writeBuffer(x)); - - return addPacketLength(buffer); -} - -interface Variable { - variableType: VARIABLE_TYPES; - name: string; - flags?: number; - refCount: number; - isConst: boolean; - children?: Variable[]; - value: any; - keyType?: VARIABLE_TYPES; -} - -export function createVariableResponse(params: { - requestId?: number; variables?: Variable[]; errorCode?: number; extraBufferData?: Buffer; includePacketLength?: boolean; -}): SmartBuffer { - let buffer = new SmartBuffer(); - - writeIfSet(params.requestId, x => buffer.writeUInt32LE(x)); - writeIfSet(params.errorCode, x => buffer.writeUInt32LE(x)); - - const variables = [...params.variables]; - for (let i = 0; i < variables.length; i++) { - const variable = variables[i]; - if (variable.children) { - variables.splice(i + 1, 0, ...variable.children); - } - } - - writeIfSet(variables?.length, x => buffer.writeUInt32LE(x)); - - while (variables.length > 0) { - const variable = variables.shift(); - let flags = 0; - if (variable.isConst) { - flags |= VARIABLE_FLAGS.isConst; - } - if (variable.children) { - flags |= VARIABLE_FLAGS.isContainer; - } - if (variable.name !== undefined) { - flags |= VARIABLE_FLAGS.isNameHere; - } - if (variable.refCount !== undefined) { - flags |= VARIABLE_FLAGS.isRefCounted; - } - if (variable.value !== undefined) { - flags |= VARIABLE_FLAGS.isValueHere; - } - buffer.writeUInt8(flags); //flags - writeIfSet(variable.variableType, x => buffer.writeUInt8(x)); //variable_type - writeIfSet(variable.name, x => buffer.writeStringNT(variable.name)); - if (variable.refCount !== undefined) { - writeIfSet(variable.refCount, x => buffer.writeUInt32LE(variable.refCount)); - } - if (variable.children) { - for (const child of variable.children) { - child.flags = (child.flags ?? 0) | VARIABLE_FLAGS.isChildKey; - } - writeIfSet(variable.keyType, x => buffer.writeUInt8(variable.keyType)); - //element_count - writeIfSet(variable.children.length, x => buffer.writeUInt32LE(variable.keyType)); - } - - switch (variable.variableType) { - case VARIABLE_TYPES.Interface: - case VARIABLE_TYPES.Object: - case VARIABLE_TYPES.String: - case VARIABLE_TYPES.Subroutine: - case VARIABLE_TYPES.Function: - buffer.writeStringNT(variable.value); - break; - case VARIABLE_TYPES.Subtyped_Object: - buffer.writeStringNT(variable.value[0]); - buffer.writeStringNT(variable.value[1]); - break; - case VARIABLE_TYPES.Boolean: - buffer.writeUInt8(variable.value ? 1 : 0); - break; - case VARIABLE_TYPES.Double: - buffer.writeDoubleLE(variable.value); - break; - case VARIABLE_TYPES.Float: - buffer.writeFloatLE(variable.value); - break; - case VARIABLE_TYPES.Integer: - buffer.writeInt32LE(variable.value); - break; - case VARIABLE_TYPES.Long_Integer: - buffer.writeBigInt64LE(variable.value); - break; - default: - //nothing to write - break; - } - } - - if (params.includePacketLength) { - buffer = addPacketLength(buffer); - } - return buffer; -} - -/** - * Contains a list of breakpoint errors - */ -export function createBreakpointErrorUpdateResponse(params: { errorCode?: number; flags?: number; breakpoint_id?: number; compile_errors?: string[]; runtime_errors?: string[]; other_errors?: string[]; extraBufferData?: Buffer; includePacketLength?: boolean }): SmartBuffer { - let buffer = new SmartBuffer(); - - writeIfSet(0, x => buffer.writeUInt32LE(x)); //request_id - writeIfSet(ERROR_CODES.OK, x => buffer.writeUInt32LE(x)); //error_code - writeIfSet(UPDATE_TYPES.BREAKPOINT_ERROR, x => buffer.writeUInt32LE(x)); //update_type - - writeIfSet(params.flags, x => buffer.writeUInt32LE(x)); //flags - - writeIfSet(params.breakpoint_id, x => buffer.writeUInt32LE(x)); //breakpoint_id - - writeIfSet(params.compile_errors?.length, x => buffer.writeUInt32LE(x)); - for (const error of params.compile_errors ?? []) { - buffer.writeStringNT(error); - } - - writeIfSet(params.runtime_errors?.length, x => buffer.writeUInt32LE(x)); - for (const error of params.runtime_errors ?? []) { - buffer.writeStringNT(error); - } - - writeIfSet(params.other_errors?.length, x => buffer.writeUInt32LE(x)); - for (const error of params.other_errors ?? []) { - buffer.writeStringNT(error); - } - - // write any extra data for testing - writeIfSet(params.extraBufferData, x => buffer.writeBuffer(x)); - - if (params.includePacketLength) { - buffer = addPacketLength(buffer); - } - return buffer; -} - -/** - * If the value is undefined or null, skip the callback. - * All other values will cause the callback to be called - */ -function writeIfSet(value: T, writer: (x: T) => R, defaultValue?: T) { - if ( - //if we have a value - (value !== undefined && value !== null) || - //we don't have a value, but we have a default value - (defaultValue !== undefined && defaultValue !== null) - ) { - return writer(value); - } -} - -/** - * Build a buffer of `byteCount` size and fill it with random data - */ -export function getRandomBuffer(byteCount: number) { - const result = new SmartBuffer(); - for (let i = 0; i < byteCount; i++) { - result.writeUInt32LE(i); - } - return result.toBuffer(); -} diff --git a/src/debugProtocol/server/DebugProtocolServer.spec.ts b/src/debugProtocol/server/DebugProtocolServer.spec.ts new file mode 100644 index 00000000..e4cdd0c3 --- /dev/null +++ b/src/debugProtocol/server/DebugProtocolServer.spec.ts @@ -0,0 +1,31 @@ +import { DebugProtocolServer } from './DebugProtocolServer'; +import * as Net from 'net'; +import { createSandbox } from 'sinon'; +import { expect } from 'chai'; +const sinon = createSandbox(); + +describe('DebugProtocolServer', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('start', () => { + it('uses default port and host when not specified', async () => { + const tcpServer = { + on: () => { }, + listen: (options, callback) => { + callback(); + } + }; + sinon.stub(Net, 'Server').returns(tcpServer); + const stub = sinon.stub(tcpServer, 'listen').callThrough(); + + const protocolServer = new DebugProtocolServer({}); + await protocolServer.start(); + expect(stub.getCall(0).args[0]).to.eql({ + port: 8081, + hostName: '0.0.0.0' + }); + }); + }); +}); diff --git a/src/debugProtocol/server/DebugProtocolServer.ts b/src/debugProtocol/server/DebugProtocolServer.ts new file mode 100644 index 00000000..46cc4134 --- /dev/null +++ b/src/debugProtocol/server/DebugProtocolServer.ts @@ -0,0 +1,367 @@ +import { EventEmitter } from 'eventemitter3'; +import * as Net from 'net'; +import { ActionQueue } from '../../managers/ActionQueue'; +import { Command, CommandCode } from '../Constants'; +import type { ProtocolRequest, ProtocolResponse } from '../events/ProtocolEvent'; +import { AddBreakpointsRequest } from '../events/requests/AddBreakpointsRequest'; +import { AddConditionalBreakpointsRequest } from '../events/requests/AddConditionalBreakpointsRequest'; +import { ContinueRequest } from '../events/requests/ContinueRequest'; +import { ExecuteRequest } from '../events/requests/ExecuteRequest'; +import { ExitChannelRequest } from '../events/requests/ExitChannelRequest'; +import { HandshakeRequest } from '../events/requests/HandshakeRequest'; +import { ListBreakpointsRequest } from '../events/requests/ListBreakpointsRequest'; +import { RemoveBreakpointsRequest } from '../events/requests/RemoveBreakpointsRequest'; +import { StackTraceRequest } from '../events/requests/StackTraceRequest'; +import { StepRequest } from '../events/requests/StepRequest'; +import { StopRequest } from '../events/requests/StopRequest'; +import { ThreadsRequest } from '../events/requests/ThreadsRequest'; +import { VariablesRequest } from '../events/requests/VariablesRequest'; +import { HandshakeResponse } from '../events/responses/HandshakeResponse'; +import { HandshakeV3Response } from '../events/responses/HandshakeV3Response'; +import PluginInterface from '../PluginInterface'; +import type { ProtocolServerPlugin } from './DebugProtocolServerPlugin'; +import { logger } from '../../logging'; +import { defer, util } from '../../util'; +import { protocolUtil } from '../ProtocolUtil'; +import { SmartBuffer } from 'smart-buffer'; + +export const DEBUGGER_MAGIC = 'bsdebug'; + +/** + * A class that emulates the way a Roku's DebugProtocol debug session/server works. This is mostly useful for unit testing, + * but might eventually be helpful for an off-device emulator as well + */ +export class DebugProtocolServer { + constructor( + public options?: DebugProtocolServerOptions + ) { + + } + + private logger = logger.createLogger(`[${DebugProtocolServer.name}]`); + + /** + * Indicates whether the client has sent the magic string to kick off the debug session. + */ + private isHandshakeComplete = false; + + private buffer = Buffer.alloc(0); + + /** + * The server + */ + private server: Net.Server; + /** + * Once a client connects, this is a reference to that client + */ + private client: Net.Socket; + + /** + * A collection of plugins that can interact with the server at lifecycle points + */ + public plugins = new PluginInterface(); + + /** + * A queue for processing the incoming buffer, every transmission at a time + */ + private bufferQueue = new ActionQueue(); + + public get controlPort() { + return this.options.controlPort ?? this._port; + } + private _port: number; + + /** + * Run the server. This opens a socket and listens for a connection. + * The promise resolves when the server has started listening. It does NOT wait for a client to connect + */ + public async start() { + const deferred = defer(); + try { + this.server = new Net.Server({}); + //Roku only allows 1 connection, so we should too. + this.server.maxConnections = 1; + + //whenever a client makes a connection + // eslint-disable-next-line @typescript-eslint/no-misused-promises + this.server.on('connection', async (socket: Net.Socket) => { + const event = await this.plugins.emit('onClientConnected', { + server: this, + client: socket + }); + this.client = event.client; + + //anytime we receive incoming data from the client + this.client.on('data', (data) => { + //queue up processing the new data, chunk by chunk + void this.bufferQueue.run(async () => { + this.buffer = Buffer.concat([this.buffer, data]); + while (this.buffer.length > 0 && await this.process()) { + //the loop condition is the actual work + } + return true; + }); + }); + //handle connection errors + this.client.on('error', (e) => { + this.logger.error(e); + }); + }); + this._port = this.controlPort ?? await util.getPort(); + + //handle connection errors + this.server.on('error', (e) => { + this.logger.error(e); + }); + + this.server.listen({ + port: this.options.controlPort ?? 8081, + hostName: this.options.host ?? '0.0.0.0' + }, () => { + void this.plugins.emit('onServerStart', { server: this }); + deferred.resolve(); + }); + } catch (e) { + deferred.reject(e); + } + return deferred.promise; + } + + public async stop() { + //close the client socket + this.client?.destroy(); + + //now close the server + try { + await new Promise((resolve, reject) => { + this.server.close((err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } finally { + this.client?.removeAllListeners(); + delete this.client; + this.server?.removeAllListeners(); + delete this.server; + } + } + + public async destroy() { + await this.stop(); + } + + /** + * Given a buffer, find the request that matches it + */ + public static getRequest(buffer: Buffer, allowHandshake: boolean): ProtocolRequest { + //when enabled, look at the start of the buffer for the exact DEBUGGER_MAGIC text. This is a boolean because + //there could be cases where binary data looks similar to this structure, so the caller must opt-in to this logic + if (allowHandshake && buffer.length >= 8 && protocolUtil.readStringNT(SmartBuffer.fromBuffer(buffer)) === DEBUGGER_MAGIC) { + return HandshakeRequest.fromBuffer(buffer); + } + // if we don't have enough buffer data, skip this + if (buffer.length < 12) { + return; + } + //the client may only send commands to the server, so extract the command type from the known byte position + const command = CommandCode[buffer.readUInt32LE(8)] as Command; // command_code + switch (command) { + case Command.AddBreakpoints: + return AddBreakpointsRequest.fromBuffer(buffer); + case Command.Stop: + return StopRequest.fromBuffer(buffer); + case Command.Continue: + return ContinueRequest.fromBuffer(buffer); + case Command.Threads: + return ThreadsRequest.fromBuffer(buffer); + case Command.StackTrace: + return StackTraceRequest.fromBuffer(buffer); + case Command.Variables: + return VariablesRequest.fromBuffer(buffer); + case Command.Step: + return StepRequest.fromBuffer(buffer); + case Command.ListBreakpoints: + return ListBreakpointsRequest.fromBuffer(buffer); + case Command.RemoveBreakpoints: + return RemoveBreakpointsRequest.fromBuffer(buffer); + case Command.Execute: + return ExecuteRequest.fromBuffer(buffer); + case Command.AddConditionalBreakpoints: + return AddConditionalBreakpointsRequest.fromBuffer(buffer); + case Command.ExitChannel: + return ExitChannelRequest.fromBuffer(buffer); + } + } + + private getResponse(request: ProtocolRequest) { + if (request instanceof HandshakeRequest) { + return HandshakeV3Response.fromJson({ + magic: this.magic, + protocolVersion: '3.1.0', + //TODO update this to an actual date from the device + revisionTimestamp: new Date(2022, 1, 1) + }); + } + } + + /** + * Process a single request. + * @returns true if successfully processed a request, and false if not + */ + private async process() { + try { + this.logger.log('process() start', { buffer: this.buffer.toJSON() }); + + //at this point, there is an active debug session. The plugin must provide us all the real-world data + let { buffer, request } = await this.plugins.emit('provideRequest', { + server: this, + buffer: this.buffer, + request: undefined + }); + + //we must build the request if the plugin didn't supply one (most plugins won't provide a request...) + if (!request) { + //if we haven't seen the handshake yet, look for the handshake first + if (!this.isHandshakeComplete) { + request = HandshakeRequest.fromBuffer(buffer); + } else { + request = DebugProtocolServer.getRequest(buffer, false); + } + } + + //if we couldn't construct a request this request, hard-fail + if (!request || !request.success) { + this.logger.error('process() invalid request', { request }); + throw new Error(`Unable to parse request: ${JSON.stringify(this.buffer.toJSON().data)}`); + } + + this.logger.log('process() constructed request', { request }); + + //trim the buffer now that the request has been processed + this.buffer = buffer.slice(request.readOffset); + + this.logger.log('process() buffer sliced', { buffer: this.buffer.toJSON() }); + + //now ask the plugin to provide a response for the given request + let { response } = await this.plugins.emit('provideResponse', { + server: this, + request: request, + response: undefined + }); + + + //if the plugin didn't provide a response, we need to try our best to make one (we only support a few...plugins should provide most of them) + if (!response) { + response = this.getResponse(request); + } + + if (!response) { + this.logger.error('process() invalid response', { request, response }); + throw new Error(`Server was unable to provide a response for ${JSON.stringify(request.data)}`); + } + + + //if this is part of the handshake flow, the client should have sent a magic string to kick off the debugger. If it matches, set `isHandshakeComplete = true` + if ((response instanceof HandshakeResponse || response instanceof HandshakeV3Response) && response.data.magic === this.magic) { + this.isHandshakeComplete = true; + } + + //send the response to the client. (TODO handle when the response is missing) + await this.sendResponse(response); + return true; + } catch (e) { + this.logger.error('process() error', e); + } + return false; + } + + /** + * Send a response from the server to the client. This involves writing the response buffer to the client socket + */ + private async sendResponse(response: ProtocolResponse) { + const event = await this.plugins.emit('beforeSendResponse', { + server: this, + response: response + }); + + this.logger.log('sendResponse()', { response }); + this.client.write(event.response.toBuffer()); + + await this.plugins.emit('afterSendResponse', { + server: this, + response: event.response + }); + return event.response; + } + + /** + * Send an update from the server to the client. This can be things like ALL_THREADS_STOPPED + */ + public sendUpdate(update: ProtocolResponse) { + return this.sendResponse(update); + } + + /** + * An event emitter used for all of the events this server emitts + */ + private emitter = new EventEmitter(); + + public on(eventName: 'before-send-response', callback: (event: T) => void); + public on(eventName: 'after-send-response', callback: (event: T) => void); + public on(eventName: 'client-connected', callback: (event: T) => void); + public on(eventName: string, callback: (data: T) => void) + public on(eventName: string, callback: (data: T) => void) { + this.emitter.on(eventName, callback); + return () => { + this.emitter?.removeListener(eventName, callback); + }; + } + + /** + * Subscribe to an event exactly one time. This will fire the very next time an event happens, + * and then immediately unsubscribe + */ + public once(eventName: string): Promise { + return new Promise((resolve) => { + const off = this.on(eventName, (event) => { + off(); + resolve(event); + }); + }); + } + + public emit(eventName: 'before-send-response', event: T): T; + public emit(eventName: 'after-send-response', event: T): T; + public emit(eventName: 'client-connected', event: T): T; + public emit(eventName: string, event: any): T { + this.emitter?.emit(eventName, event); + return event; + } + + /** + * The magic string used to kick off the debug session. + * @default "bsdebug" + */ + private get magic() { + return this.options.magic ?? DEBUGGER_MAGIC; + } +} + +export interface DebugProtocolServerOptions { + /** + * The magic that is sent as part of the handshake + */ + magic?: string; + /** + * The port to use for the primary communication between this server and a client + */ + controlPort?: number; + /** + * A specific host to listen on. If not specified, all hosts are used + */ + host?: string; +} diff --git a/src/debugProtocol/server/DebugProtocolServerPlugin.ts b/src/debugProtocol/server/DebugProtocolServerPlugin.ts new file mode 100644 index 00000000..a76e473f --- /dev/null +++ b/src/debugProtocol/server/DebugProtocolServerPlugin.ts @@ -0,0 +1,49 @@ +import type { DebugProtocolServer } from './DebugProtocolServer'; +import type { Socket } from 'net'; +import type { ProtocolRequest, ProtocolResponse } from '../events/ProtocolEvent'; +import { DebugProtocolServerTestPlugin } from '../DebugProtocolServerTestPlugin.spec'; + +export interface ProtocolServerPlugin { + onServerStart?: Handler; + onClientConnected?: Handler; + + provideRequest?: Handler; + provideResponse?: Handler; + + beforeSendResponse?: Handler; + afterSendResponse?: Handler; +} + +export interface OnServerStartEvent { + server: DebugProtocolServer; +} + +export interface OnClientConnectedEvent { + server: DebugProtocolServer; + client: Socket; +} + +export interface ProvideRequestEvent { + server: DebugProtocolServer; + buffer: Buffer; + /** + * The plugin should provide this property + */ + request?: ProtocolRequest; +} +export interface ProvideResponseEvent { + server: DebugProtocolServer; + request: ProtocolRequest; + /** + * The plugin should provide this property + */ + response?: ProtocolResponse; +} + +export interface BeforeSendResponseEvent { + server: DebugProtocolServer; + response: ProtocolResponse; +} +export type AfterSendResponseEvent = BeforeSendResponseEvent; + +export type Handler = (event: T) => R; diff --git a/src/debugSession/BrightScriptDebugSession.spec.ts b/src/debugSession/BrightScriptDebugSession.spec.ts index fee554b2..c69f9382 100644 --- a/src/debugSession/BrightScriptDebugSession.spec.ts +++ b/src/debugSession/BrightScriptDebugSession.spec.ts @@ -13,7 +13,7 @@ import { defer } from '../util'; import { HighLevelType } from '../interfaces'; import type { LaunchConfiguration } from '../LaunchConfiguration'; import type { SinonStub } from 'sinon'; -import { util as bscUtil, standardizePath as s } from 'brighterscript'; +import { DiagnosticSeverity, util as bscUtil, standardizePath as s } from 'brighterscript'; import { DefaultFiles } from 'roku-deploy'; import type { AddProjectParams, ComponentLibraryConstructorParams } from '../managers/ProjectManager'; import { ComponentLibraryProject, Project } from '../managers/ProjectManager'; @@ -142,6 +142,12 @@ describe('BrightScriptDebugSession', () => { }); }); + afterEach(() => { + fsExtra.emptydirSync(tempDir); + fsExtra.removeSync(outDir); + sinon.restore(); + }); + describe('evaluateRequest', () => { it('resets local var counter on suspend', async () => { const stub = sinon.stub(session['rokuAdapter'], 'evaluate').callsFake(x => { @@ -226,7 +232,7 @@ describe('BrightScriptDebugSession', () => { const stub = sinon.stub(session['rokuAdapter'], 'evaluate').callsFake(x => { return Promise.resolve({ type: 'message', message: '' }); }); - sinon.stub(rokuAdapter, 'getScopeVariables').callsFake(x => { + sinon.stub(rokuAdapter, 'getScopeVariables').callsFake(() => { return Promise.resolve(['m', 'top', `${session.tempVarPrefix}eval`]); }); sinon.stub(rokuAdapter, 'getVariable').callsFake(x => { @@ -705,13 +711,16 @@ describe('BrightScriptDebugSession', () => { await session['handleDiagnostics']([{ message: 'Crash', path: 'SomeComponent.xml', - range: bscUtil.createRange(1, 2, 3, 4) + range: bscUtil.createRange(1, 2, 3, 4), + severity: DiagnosticSeverity.Warning }]); expect(stub.getCall(0).args[0]?.body).to.eql({ diagnostics: [{ message: 'Crash', path: s`${stagingDir}/.roku-deploy-staging/components/SomeComponent.xml`, - range: bscUtil.createRange(1, 2, 1, 4) + range: bscUtil.createRange(1, 2, 1, 4), + severity: DiagnosticSeverity.Warning, + source: 'roku-debug' }] }); }); diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index dbdfad0f..b1c6c28a 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -26,7 +26,7 @@ import { fileUtils, standardizePath as s } from '../FileUtils'; import { ComponentLibraryServer } from '../ComponentLibraryServer'; import { ProjectManager, Project, ComponentLibraryProject } from '../managers/ProjectManager'; import type { EvaluateContainer } from '../adapters/DebugProtocolAdapter'; -import { DebugProtocolAdapter } from '../adapters/DebugProtocolAdapter'; +import { isDebugProtocolAdapter, DebugProtocolAdapter } from '../adapters/DebugProtocolAdapter'; import { TelnetAdapter } from '../adapters/TelnetAdapter'; import type { BSDebugDiagnostic } from '../CompileErrorProcessor'; import { RendezvousTracker } from '../RendezvousTracker'; @@ -48,9 +48,13 @@ import { LocationManager } from '../managers/LocationManager'; import type { AugmentedSourceBreakpoint } from '../managers/BreakpointManager'; import { BreakpointManager } from '../managers/BreakpointManager'; import type { LogMessage } from '../logging'; -import { logger, FileLoggingManager, debugServerLogOutputEventTransport } from '../logging'; +import { logger, FileLoggingManager, debugServerLogOutputEventTransport, LogLevelPriority } from '../logging'; import type { DeviceInfo } from '../DeviceInfo'; import * as xml2js from 'xml2js'; +import { VariableType } from '../debugProtocol/events/responses/VariablesResponse'; +import { DiagnosticSeverity } from 'brighterscript'; + +const diagnosticSource = 'roku-debug'; export class BrightScriptDebugSession extends BaseDebugSession { public constructor() { @@ -67,29 +71,29 @@ export class BrightScriptDebugSession extends BaseDebugSession { this.locationManager = new LocationManager(this.sourceMapManager); this.breakpointManager = new BreakpointManager(this.sourceMapManager, this.locationManager); //send newly-verified breakpoints to vscode - this.breakpointManager.on('breakpoints-verified', (data) => this.onDeviceVerifiedBreakpoints(data)); + this.breakpointManager.on('breakpoints-verified', (data) => this.onDeviceBreakpointsChanged('changed', data)); this.projectManager = new ProjectManager(this.breakpointManager, this.locationManager); this.fileLoggingManager = new FileLoggingManager(); } - private onDeviceVerifiedBreakpoints(data: { breakpoints: AugmentedSourceBreakpoint[] }) { + private onDeviceBreakpointsChanged(eventName: 'changed' | 'new', data: { breakpoints: AugmentedSourceBreakpoint[] }) { this.logger.info('Sending verified device breakpoints to client', data); //send all verified breakpoints to the client for (const breakpoint of data.breakpoints) { const event: DebugProtocol.Breakpoint = { line: breakpoint.line, column: breakpoint.column, - verified: true, + verified: breakpoint.verified, id: breakpoint.id, source: { path: breakpoint.srcPath } }; - this.sendEvent(new BreakpointEvent('changed', event)); + this.sendEvent(new BreakpointEvent(eventName, event)); } } - public logger = logger.createLogger(`[${BrightScriptDebugSession.name}]`); + public logger = logger.createLogger(`[session]`); /** * A sequence used to help identify log statements for requests @@ -132,6 +136,16 @@ export class BrightScriptDebugSession extends BaseDebugSession { public tempVarPrefix = '__rokudebug__'; + /** + * The first encountered compile error, will be used to send to the client as a runtime error (nicer UI presentation) + */ + private compileError: BSDebugDiagnostic; + + /** + * A magic number to represent a fake thread that will be used for showing compile errors in the UI as if they were runtime crashes + */ + private COMPILE_ERROR_THREAD_ID = 7_777; + private get enableDebugProtocol() { return this.launchConfiguration.enableDebugProtocol; } @@ -242,7 +256,11 @@ export class BrightScriptDebugSession extends BaseDebugSession { public deviceInfo: DeviceInfo; public async launchRequest(response: DebugProtocol.LaunchResponse, config: LaunchConfiguration) { + this.logger.log('[launchRequest] begin'); + //send the response right away so the UI immediately shows the debugger toolbar + this.sendResponse(response); + this.launchConfiguration = config; //set the logLevel provided by the launch config @@ -356,11 +374,6 @@ export class BrightScriptDebugSession extends BaseDebugSession { } }); - //ignore the compile error failure from within the publish - (this.launchConfiguration as any).failOnCompileError = false; - // Set the remote debug flag on the args to be passed to roku deploy so the socket debugger can be started if needed. - (this.launchConfiguration as any).remoteDebug = this.enableDebugProtocol; - await this.connectAndPublish(); this.sendEvent(new ChannelPublishedEvent( @@ -369,18 +382,18 @@ export class BrightScriptDebugSession extends BaseDebugSession { //tell the adapter adapter that the channel has been launched. await this.rokuAdapter.activate(); - + if (this.rokuAdapter.isDestroyed) { + throw new Error('Debug session encountered an error'); + } if (!error) { if (this.rokuAdapter.connected) { this.logger.info('Host connection was established before the main public process was completed'); this.logger.log(`deployed to Roku@${this.launchConfiguration.host}`); - this.sendResponse(response); } else { this.logger.info('Main public process was completed but we are still waiting for a connection to the host'); this.rokuAdapter.on('connected', (status) => { if (status) { this.logger.log(`deployed to Roku@${this.launchConfiguration.host}`); - this.sendResponse(response); } }); } @@ -391,14 +404,12 @@ export class BrightScriptDebugSession extends BaseDebugSession { //if the message is anything other than compile errors, we want to display the error if (!(e instanceof CompileError)) { util.log('Encountered an issue during the publish process'); - util.log((e as Error).message); - this.sendErrorResponse(response, -1, (e as Error).message); - } + util.log((e as Error)?.stack); + this.sendErrorResponse(response, -1, (e as Error)?.stack); - //send any compile errors to the client - await this.rokuAdapter.sendErrors(); - this.logger.error('Error. Shutting down.', e); - return this.shutdown(); + //send any compile errors to the client + await this.rokuAdapter?.sendErrors(); + } } //at this point, the project has been deployed. If we need to use a deep link, launch it now. @@ -463,6 +474,7 @@ export class BrightScriptDebugSession extends BaseDebugSession { private async handleDiagnostics(diagnostics: BSDebugDiagnostic[]) { // Roku device and sourcemap work with 1-based line numbers, VSCode expects 0-based lines. for (let diagnostic of diagnostics) { + diagnostic.source = diagnosticSource; let sourceLocation = await this.projectManager.getSourceLocation(diagnostic.path, diagnostic.range.start.line + 1); if (sourceLocation) { diagnostic.path = sourceLocation.filePath; @@ -474,9 +486,17 @@ export class BrightScriptDebugSession extends BaseDebugSession { } } + //find the first compile error (i.e. first DiagnosticSeverity.Error) if there is one + this.compileError = diagnostics.find(x => x.severity === DiagnosticSeverity.Error); + if (this.compileError) { + this.sendEvent(new StoppedEvent( + StoppedEventReason.exception, + this.COMPILE_ERROR_THREAD_ID, + `CompileError: ${this.compileError.message}` + )); + } + this.sendEvent(new DiagnosticsEvent(diagnostics)); - //stop the roku adapter and exit the channel - return this.shutdown(); } private async connectAndPublish() { @@ -500,9 +520,18 @@ export class BrightScriptDebugSession extends BaseDebugSession { //publish the package to the target Roku const publishPromise = this.rokuDeploy.publish({ ...this.launchConfiguration, + //typing fix + logLevel: LogLevelPriority[this.logger.logLevel], + // enable the debug protocol if true + remoteDebug: this.enableDebugProtocol, + //necessary for capturing compile errors from the protocol (has no effect on telnet) + remoteDebugConnectEarly: false, + //we don't want to fail if there were compile errors...we'll let our compile error processor handle that failOnCompileError: true - } as any as RokuDeployOptions).then(() => { + }).then(() => { packageIsPublished = true; + }).catch((e) => { + this.logger.error(e); }); await publishPromise; @@ -718,6 +747,7 @@ export class BrightScriptDebugSession extends BaseDebugSession { * Called every time a breakpoint is created, modified, or deleted, for each file. This receives the entire list of breakpoints every time. */ public async setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments) { + this.logger.log('setBreakpointsRequest'); let sanitizedBreakpoints = this.breakpointManager.replaceBreakpoints(args.source.path, args.breakpoints); //sort the breakpoints let sortedAndFilteredBreakpoints = orderBy(sanitizedBreakpoints, [x => x.line, x => x.column]); @@ -727,6 +757,7 @@ export class BrightScriptDebugSession extends BaseDebugSession { }; this.sendResponse(response); + this.logger.debug('[setBreakpointsRequest] syncBreakpoints()', args); await this.rokuAdapter?.syncBreakpoints(); } @@ -741,17 +772,23 @@ export class BrightScriptDebugSession extends BaseDebugSession { let threads = []; - //only send the threads request if we are at the debugger prompt - if (this.rokuAdapter.isAtDebuggerPrompt) { - let rokuThreads = await this.rokuAdapter.getThreads(); + //This is a bit of a hack. If there's a compile error, send a thread to represent it so we can show the compile error like a runtime exception + if (this.compileError) { + threads.push(new Thread(this.COMPILE_ERROR_THREAD_ID, 'Compile Error')); + } else { + //only send the threads request if we are at the debugger prompt + if (this.rokuAdapter.isAtDebuggerPrompt) { + let rokuThreads = await this.rokuAdapter.getThreads(); - for (let thread of rokuThreads) { - threads.push( - new Thread(thread.threadId, `Thread ${thread.threadId}`) - ); + for (let thread of rokuThreads) { + threads.push( + new Thread(thread.threadId, `Thread ${thread.threadId}`) + ); + } + } else { + this.logger.log('Skipped getting threads because the RokuAdapter is not accepting input at this time.'); } - } else { - this.logger.log('Skipped getting threads because the RokuAdapter is not accepting input at this time.'); + } response.body = { @@ -766,48 +803,60 @@ export class BrightScriptDebugSession extends BaseDebugSession { this.logger.log('stackTraceRequest'); let frames = []; - if (this.rokuAdapter.isAtDebuggerPrompt) { - let stackTrace = await this.rokuAdapter.getStackTrace(args.threadId); - - for (let debugFrame of stackTrace) { - let sourceLocation = await this.projectManager.getSourceLocation(debugFrame.filePath, debugFrame.lineNumber); - - //the stacktrace returns function identifiers in all lower case. Try to get the actual case - //load the contents of the file and get the correct casing for the function identifier - try { - let functionName = this.fileManager.getCorrectFunctionNameCase(sourceLocation?.filePath, debugFrame.functionIdentifier); - if (functionName) { - - //search for original function name if this is an anonymous function. - //anonymous function names are prefixed with $ in the stack trace (i.e. $anon_1 or $functionname_40002) - if (functionName.startsWith('$')) { - functionName = this.fileManager.getFunctionNameAtPosition( - sourceLocation.filePath, - sourceLocation.lineNumber - 1, - functionName - ); + //this is a bit of a hack. If there's a compile error, send a full stack frame so we can show the compile error like a runtime crash + if (this.compileError) { + frames.push(new StackFrame( + 0, + 'Compile Error', + new Source(path.basename(this.compileError.path), this.compileError.path), + //diagnostics are 0 based, vscode expects 1 based + this.compileError.range.start.line + 1, + this.compileError.range.start.character + 1 + )); + } else { + if (this.rokuAdapter.isAtDebuggerPrompt) { + let stackTrace = await this.rokuAdapter.getStackTrace(args.threadId); + + for (let debugFrame of stackTrace) { + let sourceLocation = await this.projectManager.getSourceLocation(debugFrame.filePath, debugFrame.lineNumber); + + //the stacktrace returns function identifiers in all lower case. Try to get the actual case + //load the contents of the file and get the correct casing for the function identifier + try { + let functionName = this.fileManager.getCorrectFunctionNameCase(sourceLocation?.filePath, debugFrame.functionIdentifier); + if (functionName) { + + //search for original function name if this is an anonymous function. + //anonymous function names are prefixed with $ in the stack trace (i.e. $anon_1 or $functionname_40002) + if (functionName.startsWith('$')) { + functionName = this.fileManager.getFunctionNameAtPosition( + sourceLocation.filePath, + sourceLocation.lineNumber - 1, + functionName + ); + } + debugFrame.functionIdentifier = functionName; } - debugFrame.functionIdentifier = functionName; + } catch (error) { + this.logger.error('Error correcting function identifier case', { error, sourceLocation, debugFrame }); } - } catch (error) { - this.logger.error('Error correcting function identifier case', { error, sourceLocation, debugFrame }); - } - const filePath = sourceLocation?.filePath ?? debugFrame.filePath; - - const frame: DebugProtocol.StackFrame = new StackFrame( - debugFrame.frameId, - `${debugFrame.functionIdentifier}`, - new Source(path.basename(filePath), filePath), - sourceLocation?.lineNumber ?? debugFrame.lineNumber, - 1 - ); - if (!sourceLocation) { - frame.presentationHint = 'subtle'; + const filePath = sourceLocation?.filePath ?? debugFrame.filePath; + + const frame: DebugProtocol.StackFrame = new StackFrame( + debugFrame.frameId, + `${debugFrame.functionIdentifier}`, + new Source(path.basename(filePath), filePath), + sourceLocation?.lineNumber ?? debugFrame.lineNumber, + 1 + ); + if (!sourceLocation) { + frame.presentationHint = 'subtle'; + } + frames.push(frame); } - frames.push(frame); + } else { + this.logger.log('Skipped calculating stacktrace because the RokuAdapter is not accepting input at this time'); } - } else { - this.logger.log('Skipped calculating stacktrace because the RokuAdapter is not accepting input at this time'); } response.body = { stackFrames: frames, @@ -825,14 +874,14 @@ export class BrightScriptDebugSession extends BaseDebugSession { try { const scopes = new Array(); - if (this.enableDebugProtocol) { + if (isDebugProtocolAdapter(this.rokuAdapter)) { let refId = this.getEvaluateRefId('', args.frameId); let v: AugmentedVariable; //if we already looked this item up, return it if (this.variables[refId]) { v = this.variables[refId]; } else { - let result = await this.rokuAdapter.getVariable('', args.frameId, true); + let result = await this.rokuAdapter.getLocalVariables(args.frameId); if (!result) { throw new Error(`Could not get scopes`); } @@ -862,6 +911,13 @@ export class BrightScriptDebugSession extends BaseDebugSession { } protected async continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) { + //if we have a compile error, we should shut down + if (this.compileError) { + this.sendResponse(response); + await this.shutdown(); + return; + } + this.logger.log('continueRequest'); await this.rokuAdapter.continue(); this.sendResponse(response); @@ -869,6 +925,14 @@ export class BrightScriptDebugSession extends BaseDebugSession { protected async pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) { this.logger.log('pauseRequest'); + + //if we have a compile error, we should shut down + if (this.compileError) { + this.sendResponse(response); + await this.shutdown(); + return; + } + await this.rokuAdapter.pause(); this.sendResponse(response); } @@ -885,6 +949,14 @@ export class BrightScriptDebugSession extends BaseDebugSession { */ protected async nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) { this.logger.log('[nextRequest] begin'); + + //if we have a compile error, we should shut down + if (this.compileError) { + this.sendResponse(response); + await this.shutdown(); + return; + } + try { await this.rokuAdapter.stepOver(args.threadId); this.logger.info('[nextRequest] end'); @@ -896,6 +968,14 @@ export class BrightScriptDebugSession extends BaseDebugSession { protected async stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) { this.logger.log('[stepInRequest]'); + + //if we have a compile error, we should shut down + if (this.compileError) { + this.sendResponse(response); + await this.shutdown(); + return; + } + await this.rokuAdapter.stepInto(args.threadId); this.sendResponse(response); this.logger.info('[stepInRequest] end'); @@ -903,6 +983,14 @@ export class BrightScriptDebugSession extends BaseDebugSession { protected async stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) { this.logger.log('[stepOutRequest] begin'); + + //if we have a compile error, we should shut down + if (this.compileError) { + this.sendResponse(response); + await this.shutdown(); + return; + } + await this.rokuAdapter.stepOut(args.threadId); this.sendResponse(response); this.logger.info('[stepOutRequest] end'); @@ -933,7 +1021,7 @@ export class BrightScriptDebugSession extends BaseDebugSession { logger.log('reference', reference); // NOTE: Legacy telnet support for local vars if (this.launchConfiguration.enableVariablesPanel) { - const vars = await (this.rokuAdapter as TelnetAdapter).getScopeVariables(reference); + const vars = await (this.rokuAdapter as TelnetAdapter).getScopeVariables(); for (const varName of vars) { let result = await this.rokuAdapter.getVariable(varName, -1); @@ -1036,11 +1124,15 @@ export class BrightScriptDebugSession extends BaseDebugSession { let varIndex = this.getNextVarIndex(args.frameId); let arrayVarName = this.tempVarPrefix + 'eval'; if (varIndex === 0) { - await this.rokuAdapter.evaluate(`${arrayVarName} = []`, args.frameId); + const response = await this.rokuAdapter.evaluate(`${arrayVarName} = []`, args.frameId); + console.log(response); } let statement = `${arrayVarName}[${varIndex}] = ${args.expression}`; args.expression = `${arrayVarName}[${varIndex}]`; let commandResults = await this.rokuAdapter.evaluate(statement, args.frameId); + if (commandResults.type === 'error') { + throw new Error(commandResults.message); + } variablePath = [arrayVarName, varIndex.toString()]; } @@ -1052,10 +1144,11 @@ export class BrightScriptDebugSession extends BaseDebugSession { if (this.variables[refId]) { v = this.variables[refId]; } else { - let result = await this.rokuAdapter.getVariable(args.expression, args.frameId, true); + let result = await this.rokuAdapter.getVariable(args.expression, args.frameId); if (!result) { - throw new Error(`bad variable request "${args.expression}"`); + throw new Error('Error: unable to evaluate expression'); } + v = this.getVariableFromResult(result, args.frameId); //TODO - testing something, remove later // eslint-disable-next-line camelcase @@ -1100,8 +1193,9 @@ export class BrightScriptDebugSession extends BaseDebugSession { } } catch (error) { this.logger.error('Error during variables request', error); + response.success = false; + response.message = error?.message ?? error; } - // try { this.sendResponse(response); } catch { } @@ -1114,20 +1208,17 @@ export class BrightScriptDebugSession extends BaseDebugSession { * @param args */ protected async disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments, request?: DebugProtocol.Request) { - if (this.rokuAdapter) { - await this.rokuAdapter.destroy(); - } //return to the home screen if (!this.enableDebugProtocol) { await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort); } - this.componentLibraryServer.stop(); this.sendResponse(response); + await this.shutdown(); } private createRokuAdapter(host: string, rendezvousTracker: RendezvousTracker) { if (this.enableDebugProtocol) { - this.rokuAdapter = new DebugProtocolAdapter(this.launchConfiguration, this.projectManager, this.breakpointManager, rendezvousTracker); + this.rokuAdapter = new DebugProtocolAdapter(this.launchConfiguration, this.projectManager, this.breakpointManager, rendezvousTracker, this.deviceInfo); } else { this.rokuAdapter = new TelnetAdapter(this.launchConfiguration, rendezvousTracker); } @@ -1190,10 +1281,6 @@ export class BrightScriptDebugSession extends BaseDebugSession { //clear the index for storing evalutated expressions this.evaluateVarIndexByFrameId.clear(); - //sync breakpoints - await this.rokuAdapter?.syncBreakpoints(); - this.logger.info('received "suspend" event from adapter'); - const threads = await this.rokuAdapter.getThreads(); const activeThread = threads.find(x => x.isSelected); @@ -1210,6 +1297,23 @@ export class BrightScriptDebugSession extends BaseDebugSession { }) ); + outer: for (const bp of this.breakpointManager.failedDeletions) { + for (const thread of threads) { + let sourceLocation = await this.projectManager.getSourceLocation(thread.filePath, thread.lineNumber); + // This stop was due to a breakpoint that we tried to delete, but couldn't. + // Now that we are stopped, we can delete it. We won't stop here again unless you re-add the breakpoint. You're welcome. + if ((bp.srcPath === sourceLocation.filePath) && (bp.line === sourceLocation.lineNumber)) { + this.showPopupMessage(`Stopped at breakpoint that failed to delete. Deleting now, and should not cause future stops.`, 'info'); + this.logger.warn(`Stopped at breakpoint that failed to delete. Deleting now, and should not cause future stops`, bp, thread, sourceLocation); + break outer; + } + } + } + + //sync breakpoints + await this.rokuAdapter?.syncBreakpoints(); + this.logger.info('received "suspend" event from adapter'); + //if !stopOnEntry, and we haven't encountered a suspend yet, THIS is the entry breakpoint. auto-continue if (!this.entryBreakpointWasHandled && !this.launchConfiguration.stopOnEntry) { this.entryBreakpointWasHandled = true; @@ -1250,7 +1354,15 @@ export class BrightScriptDebugSession extends BaseDebugSession { v = new Variable(result.name, result.type, refId, 0, result.elementCount); } } else { - v = new Variable(result.name, `${result.value}`); + let value: string; + if (result.type === VariableType.Invalid) { + value = result.value ?? 'Invalid'; + } else if (result.type === VariableType.Uninitialized) { + value = 'Uninitialized'; + } else { + value = `${result.value}`; + } + v = new Variable(result.name, value); } this.variables[refId] = v; } else { @@ -1286,6 +1398,10 @@ export class BrightScriptDebugSession extends BaseDebugSession { } v.childVariables = childVariables; } + // if the var is an array and debugProtocol is enabled, include the array size + if (this.enableDebugProtocol && v.type === VariableType.Array) { + v.value = `${v.type}(${result.elementCount})` as any; + } } return v; } @@ -1346,7 +1462,8 @@ export class BrightScriptDebugSession extends BaseDebugSession { private async _shutdown(errorMessage?: string): Promise { try { - // + this.componentLibraryServer?.stop(); + this.rendezvousTracker?.destroy?.(); //if configured, delete the staging directory @@ -1369,19 +1486,17 @@ export class BrightScriptDebugSession extends BaseDebugSession { this.showPopupMessage(errorMessage, 'error'); } - if (this.launchConfiguration.stopDebuggerOnAppExit !== false) { - this.logger.log('Destroy rokuAdapter'); - await this.rokuAdapter?.destroy?.(); - //press the home button to return to the home screen - try { - this.logger.log('Press home button'); - await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort); - } catch (e) { - console.error(e); - this.logger.error(e); - } + this.logger.log('Destroy rokuAdapter'); + await this.rokuAdapter?.destroy?.(); + //press the home button to return to the home screen + try { + this.logger.log('Press home button'); + await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort); + } catch (e) { + this.logger.error(e); } + this.logger.log('Send terminated event'); this.sendEvent(new TerminatedEvent()); diff --git a/src/index.ts b/src/index.ts index a1a6abef..c48abc78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,12 @@ export * from './managers/BreakpointManager'; export * from './LaunchConfiguration'; -export * from './debugProtocol/Debugger'; +export * from './debugProtocol/client/DebugProtocolClient'; export * from './debugSession/BrightScriptDebugSession'; export * from './debugSession/Events'; export * from './ComponentLibraryServer'; export * from './CompileErrorProcessor'; export * from './debugProtocol/Constants'; -export * from './debugProtocol/Debugger'; -export * from './debugProtocol/responses'; +export * from './debugProtocol/client/DebugProtocolClient'; export * from './FileUtils'; export * from './managers/ProjectManager'; export * from './RendezvousTracker'; diff --git a/src/logging.ts b/src/logging.ts index cbc8fbec..4bb0f426 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -9,7 +9,7 @@ import * as fsExtra from 'fs-extra'; import * as dateformat from 'dateformat'; import { standardizePath as s } from './FileUtils'; -const logger = defaultLogger.createLogger('[roku-debug]'); +const logger = defaultLogger.createLogger('[dap]'); //disable colors logger.enableColor = false; @@ -30,6 +30,7 @@ const createLogger = logger.createLogger.bind(logger) as typeof Logger.prototype export { logger, createLogger }; export type { Logger, LogMessage, LogLevel } from '@rokucommunity/logger'; +export { LogLevelPriority } from '@rokucommunity/logger'; export class FileLoggingManager { diff --git a/src/managers/ActionQueue.spec.ts b/src/managers/ActionQueue.spec.ts new file mode 100644 index 00000000..8d55d4dd --- /dev/null +++ b/src/managers/ActionQueue.spec.ts @@ -0,0 +1,17 @@ +import { expect } from 'chai'; +import { expectThrowsAsync } from '../testHelpers.spec'; +import { ActionQueue } from './ActionQueue'; + +describe('ActionQueue', () => { + it('rejects after maxTries is reached', async () => { + const queue = new ActionQueue(); + let count = 0; + await expectThrowsAsync(async () => { + return queue.run(() => { + count++; + return false; + }, 3); + }, 'Exceeded the 3 maximum tries for this ActionQueue action'); + expect(count).to.eql(3); + }); +}); diff --git a/src/managers/ActionQueue.ts b/src/managers/ActionQueue.ts index e716c298..94d31f73 100644 --- a/src/managers/ActionQueue.ts +++ b/src/managers/ActionQueue.ts @@ -8,31 +8,59 @@ import { defer } from '../util'; export class ActionQueue { private queueItems: Array<{ - action: () => Promise; + action: () => boolean | Promise; deferred: Deferred; + maxTries: number; + tryCount: number; }> = []; - public async run(action: () => Promise) { - this.queueItems.push({ + /** + * Run an action in the queue. + * @param action return true or Promise to mark the action as finished + */ + public async run(action: () => boolean | Promise, maxTries: number = undefined) { + const queueItem = { action: action, - deferred: defer() - }); + deferred: defer(), + maxTries: maxTries, + tryCount: 0 + }; + this.queueItems.push(queueItem); await this._runActions(); + return queueItem.deferred.promise; } + private isRunning = false; + private async _runActions() { + if (this.isRunning) { + return; + } + this.isRunning = true; while (this.queueItems.length > 0) { const queueItem = this.queueItems[0]; try { - const isFinished = await queueItem.action(); + queueItem.tryCount++; + const isFinished = await Promise.resolve( + queueItem.action() + ); + if (isFinished) { this.queueItems.shift(); queueItem.deferred.resolve(); + } else if (typeof queueItem.maxTries === 'number' && queueItem.tryCount >= queueItem.maxTries) { + throw new Error(`Exceeded the ${queueItem.maxTries} maximum tries for this ActionQueue action`); } } catch (error) { this.queueItems.shift(); queueItem.deferred.reject(error); } } + this.isRunning = false; + } + + public destroy() { + this.isRunning = false; + this.queueItems = []; } } diff --git a/src/managers/BreakpointManager.spec.ts b/src/managers/BreakpointManager.spec.ts index 527b7cb7..746a611b 100644 --- a/src/managers/BreakpointManager.spec.ts +++ b/src/managers/BreakpointManager.spec.ts @@ -72,6 +72,76 @@ describe('BreakpointManager', () => { fsExtra.removeSync(tmpDir); }); + describe('pending breakpoints', () => { + it('marks existing breakpoints as pending', () => { + const breakpoints = bpManager.replaceBreakpoints(srcPath, [ + { line: 1 }, + { line: 2 }, + { line: 3 }, + { line: 4 } + ]); + bpManager.setPending(srcPath, breakpoints, false); + expect(breakpoints.map(x => bpManager.isPending(x.srcHash))).to.eql([false, false, false, false]); + + bpManager.setPending(srcPath, [{ line: 1 }, { line: 3 }], true); + + expect(breakpoints.map(x => bpManager.isPending(x.srcHash))).to.eql([true, false, true, false]); + }); + + it('marks existing breakpoints as not pending', () => { + const breakpoints = bpManager.replaceBreakpoints(srcPath, [ + { line: 1 }, + { line: 2 }, + { line: 3 }, + { line: 4 } + ]); + bpManager.setPending(srcPath, breakpoints, true); + expect(breakpoints.map(x => bpManager.isPending(x.srcHash))).to.eql([true, true, true, true]); + + bpManager.setPending(srcPath, [{ line: 1 }, { line: 3 }], false); + + expect(breakpoints.map(x => bpManager.isPending(x.srcHash))).to.eql([false, true, false, true]); + }); + + it('ignores not-found breakpoints', () => { + const breakpoints = bpManager.replaceBreakpoints(srcPath, [ + { line: 1 }, + { line: 2 }, + { line: 3 }, + { line: 4 } + ]); + bpManager.setPending(srcPath, breakpoints, true); + expect(breakpoints.map(x => bpManager.isPending(x.srcHash))).to.eql([true, true, true, true]); + + bpManager.setPending(srcPath, [{ line: 5 }], false); + + expect(breakpoints.map(x => bpManager.isPending(x.srcHash))).to.eql([true, true, true, true]); + }); + + it('remembers a breakpoint pending status through delete and add', () => { + let breakpoints = bpManager.replaceBreakpoints(srcPath, [ + { line: 1 } + ]); + //mark the breakpoint as pending + bpManager.setPending(srcPath, breakpoints, true); + expect(bpManager.isPending(srcPath, breakpoints[0])).to.be.true; + + //delete the breakpoint + bpManager.deleteBreakpoint(srcPath, { line: 1 }); + + //mark the breakpoint as pending (even though it's not there anymore) + bpManager.setPending(srcPath, [{ line: 5 }], true); + + //add the breakpoint again + breakpoints = bpManager.replaceBreakpoints(srcPath, [ + { line: 1 } + ]); + + //the breakpoint should be pending even though this is a new instance of the breakpoint + expect(bpManager.isPending(srcPath, breakpoints[0])).to.be.true; + }); + }); + describe('sanitizeSourceFilePath', () => { it('returns the original string when no key was found', () => { expect(bpManager.sanitizeSourceFilePath('a/b/c')).to.equal(s`a/b/c`); @@ -773,7 +843,7 @@ describe('BreakpointManager', () => { }, { line: 6, logMessage: 'hello world' - }]).map(x => x.hash).sort() + }]).map(x => x.srcHash).sort() ).to.eql([ s`${rootDir}/source/main.brs:2:0-standard`, s`${rootDir}/source/main.brs:3:0-condition=true`, @@ -792,7 +862,7 @@ describe('BreakpointManager', () => { line: 2 }); expect( - bpManager['getBreakpointsForFile'](pkgPath).map(x => x.hash) + bpManager['getBreakpointsForFile'](pkgPath).map(x => x.srcHash) ).to.eql([ s`${pkgPath}:2:0-standard` ]); @@ -805,7 +875,7 @@ describe('BreakpointManager', () => { line: 2 }); expect( - bpManager['getBreakpointsForFile'](pkgPath).map(x => x.hash) + bpManager['getBreakpointsForFile'](pkgPath).map(x => x.srcHash) ).to.eql([ s`${pkgPath}:2:0-standard` ]); @@ -815,7 +885,7 @@ describe('BreakpointManager', () => { condition: 'true' }); expect( - bpManager['getBreakpointsForFile'](pkgPath).map(x => x.hash) + bpManager['getBreakpointsForFile'](pkgPath).map(x => x.srcHash) ).to.eql([ s`${pkgPath}:2:0-condition=true` ]); @@ -825,7 +895,7 @@ describe('BreakpointManager', () => { hitCondition: '4' }); expect( - bpManager['getBreakpointsForFile'](pkgPath).map(x => x.hash) + bpManager['getBreakpointsForFile'](pkgPath).map(x => x.srcHash) ).to.eql([ s`${pkgPath}:2:0-hitCondition=4` ]); @@ -1041,5 +1111,19 @@ describe('BreakpointManager', () => { }); }); + it('includes the deviceId in all breakpoints when possible', async () => { + const bp = bpManager.setBreakpoint(srcPath, { line: 1 }); + + let diff = await bpManager.getDiff(projectManager.getAllProjects()); + expect(diff.added[0].deviceId).not.to.exist; + + bpManager.setBreakpointDeviceId(bp.srcHash, diff.added[0].destHash, 3); + + bpManager.deleteBreakpoint(srcPath, bp); + + diff = await bpManager.getDiff(projectManager.getAllProjects()); + + expect(diff.removed[0].deviceId).to.eql(3); + }); }); }); diff --git a/src/managers/BreakpointManager.ts b/src/managers/BreakpointManager.ts index b5fef312..be71a303 100644 --- a/src/managers/BreakpointManager.ts +++ b/src/managers/BreakpointManager.ts @@ -9,7 +9,6 @@ import { standardizePath as s } from 'roku-deploy'; import type { SourceMapManager } from './SourceMapManager'; import type { LocationManager } from './LocationManager'; import { util } from '../util'; -import { nextTick } from 'process'; import { EventEmitter } from 'eventemitter3'; export class BreakpointManager { @@ -45,6 +44,19 @@ export class BreakpointManager { }; } + /** + * Get a promise that resolves the next time the specified event occurs + */ + public once(eventName: 'breakpoints-verified'): Promise<{ breakpoints: AugmentedSourceBreakpoint[] }>; + public once(eventName: string): Promise { + return new Promise((resolve) => { + const disconnect = this.on(eventName as 'breakpoints-verified', (data) => { + disconnect(); + resolve(data); + }); + }); + } + /** * A map of breakpoints by what file they were set in. * This does not handle any source-to-dest mapping...these breakpoints are stored in the file they were set in. @@ -53,6 +65,11 @@ export class BreakpointManager { */ private breakpointsByFilePath = new Map(); + /** + * A list of breakpoints that failed to delete and will be deleted as soon as possible + */ + public failedDeletions = [] as BreakpointWorkItem[]; + /** * A sequence used to generate unique client breakpoint IDs */ @@ -95,8 +112,8 @@ export class BreakpointManager { ...breakpoint, srcPath: srcPath, //assign a hash-like key to this breakpoint (so we can match against other similar breakpoints in the future) - hash: this.getBreakpointKey(srcPath, breakpoint) - }) as AugmentedSourceBreakpoint; + srcHash: this.getBreakpointSrcHash(srcPath, breakpoint) + } as AugmentedSourceBreakpoint); //generate a new id for this breakpoint if one does not exist bp.id ??= this.breakpointIdSequence++; @@ -105,7 +122,7 @@ export class BreakpointManager { bp.verified ??= false; //if the breakpoint hash changed, mark the breakpoint as unverified - if (existingBreakpoint?.hash !== bp.hash) { + if (existingBreakpoint?.srcHash !== bp.srcHash) { bp.verified = false; } @@ -115,69 +132,108 @@ export class BreakpointManager { } //if this is one of the permanent breakpoints, mark it as verified immediately (only applicable to telnet sessions) - if (this.getPermanentBreakpoint(bp.hash)) { - this.setBreakpointDeviceId(bp.hash, bp.id); + if (this.getPermanentBreakpoint(bp.srcHash)) { + this.setBreakpointDeviceId(bp.srcHash, bp.srcHash, bp.id); this.verifyBreakpoint(bp.id, true); } return bp; } /** - * Find a breakpoint by its hash - * @returns the breakpoint, or undefined if not found + * Delete a breakpoint */ - private getBreakpointByHash(hash: string) { - return this.getBreakpointsByHashes([hash])[0]; + public deleteBreakpoint(hash: string); + public deleteBreakpoint(breakpoint: AugmentedSourceBreakpoint); + public deleteBreakpoint(srcPath: string, breakpoint: Breakpoint); + public deleteBreakpoint(...args: [string] | [AugmentedSourceBreakpoint] | [string, Breakpoint]) { + this.deleteBreakpoints([ + this.getBreakpoint(...args as [string]) + ]); } /** - * Find a list of breakpoints by their hashes - * @returns the breakpoint, or undefined if not found + * Delete a set of breakpoints */ - private getBreakpointsByHashes(hashes: string[]) { - const result = [] as AugmentedSourceBreakpoint[]; - for (const [, breakpoints] of this.breakpointsByFilePath) { - for (const breakpoint of breakpoints) { - if (hashes.includes(breakpoint.hash)) { - result.push(breakpoint); - } + public deleteBreakpoints(args: BreakpointRef[]) { + for (const breakpoint of this.getBreakpoints(args)) { + const actualBreakpoint = this.getBreakpoint(breakpoint); + if (actualBreakpoint) { + const breakpoints = new Set(this.getBreakpointsForFile(actualBreakpoint.srcPath)); + breakpoints.delete(actualBreakpoint); + this.replaceBreakpoints(actualBreakpoint.srcPath, [...breakpoints]); } } - return result; } /** - * Find a breakpoint by its deviceId - * @returns the breakpoint, or undefined if not found - */ - private getBreakpointByDeviceId(deviceId: number) { - return this.getBreakpointsByDeviceIds([deviceId])[0]; + * Get a breakpoint by providing the data you have available + */ + public getBreakpoint(hash: BreakpointRef): AugmentedSourceBreakpoint; + public getBreakpoint(srcPath: string, breakpoint: Breakpoint): AugmentedSourceBreakpoint; + public getBreakpoint(...args: [BreakpointRef] | [string, Breakpoint]): AugmentedSourceBreakpoint { + let ref: BreakpointRef; + if (typeof args[0] === 'string' && typeof args[1] === 'object') { + ref = this.getBreakpointSrcHash(args[0], args[1]); + } else { + ref = args[0]; + } + return this.getBreakpoints([ref])[0]; } /** - * Find a list of breakpoints by their deviceIds - * @returns the breakpoints, or undefined if not found + * Given a breakpoint ref, turn it into a hash */ - private getBreakpointsByDeviceIds(deviceIds: number[]) { - const result = [] as AugmentedSourceBreakpoint[]; - for (const [, breakpoints] of this.breakpointsByFilePath) { - for (const breakpoint of breakpoints) { - if (deviceIds.includes(breakpoint.deviceId)) { - result.push(breakpoint); - } - } + private refToHash(ref: BreakpointRef): string { + if (!ref) { + return; } - return result; + //hash + if (typeof ref === 'string') { + return ref; + } + //object with a .hash key + if ('srcHash' in ref) { + return ref.srcHash; + } + //breakpoint with srcPath + if (ref?.srcPath) { + return this.getBreakpointSrcHash(ref.srcPath, ref); + } + } + + /** + * Get breakpoints by providing a list of breakpoint refs + * @param refs a list of breakpoint refs for breakpoints to get + * @param includeHistoric if true, will also look through historic breakpoints for a match. + */ + public getBreakpoints(refs: BreakpointRef[]): AugmentedSourceBreakpoint[] { + //convert all refs into a hash + const refHashes = new Set(refs.map(x => this.refToHash(x))); + + //find all the breakpoints that match one of the specified refs + return [...this.breakpointsByFilePath].map(x => x[1]).flat().filter((x) => { + return refHashes.has(x.srcHash); + }); + } + + private deviceIdByDestHash = new Map(); + + /** + * Find a breakpoint by its deviceId + * @returns the breakpoint, or undefined if not found + */ + private getBreakpointByDeviceId(deviceId: number) { + const bpRef = [...this.deviceIdByDestHash.values()].find(x => { + return x.deviceId === deviceId; + }); + return this.getBreakpoint(bpRef?.srcHash); } /** * Set the deviceId of a breakpoint */ - public setBreakpointDeviceId(hash: string, deviceId: number) { - const breakpoint = this.getBreakpointByHash(hash); - if (breakpoint) { - breakpoint.deviceId = deviceId; - } + public setBreakpointDeviceId(srcHash: string, destHast: string, deviceId: number) { + this.deviceIdByDestHash.set(destHast, { srcHash: srcHash, deviceId: deviceId }); } /** @@ -187,41 +243,48 @@ export class BreakpointManager { const breakpoint = this.getBreakpointByDeviceId(deviceId); if (breakpoint) { breakpoint.verified = isVerified; - this.queueVerifyEvent(breakpoint.hash); + + this.queueEvent('breakpoints-verified', breakpoint.srcHash); + return true; + } else { + //couldn't find the breakpoint. return false so the caller can handle that properly + return false; } - //TODO handle the else case, (might be caused by timing issues?) } + private queueEventStates = new Map(); + + /** - * Whenever breakpoints get verified, they need to be synced back to vscode. - * This queues up a future function that will emit a batch of all verified breakpoints. + * Queue future events to be fired when data settles. Typically this is data that needs synced back to vscode. + * This queues up a future function that will emit a batch of all the specified breakpoints. * @param hash the breakpoint hash that identifies this specific breakpoint based on its features */ - private queueVerifyEvent(hash: string) { - this.verifiedBreakpointKeys.push(hash); - if (!this.isVerifyEventQueued) { - this.isVerifyEventQueued = true; + private queueEvent(event: 'breakpoints-verified', ref: BreakpointRef) { + //get the state (or create a new one) + const state = this.queueEventStates.get(event) ?? (this.queueEventStates.set(event, { pendingRefs: [], isQueued: false }).get(event)); + + this.queueEventStates.set(event, state); + state.pendingRefs.push(ref); + if (!state.isQueued) { + state.isQueued = true; process.nextTick(() => { - this.isVerifyEventQueued = false; - const breakpoints = this.getBreakpointsByHashes( - this.verifiedBreakpointKeys.map(x => x) - ); - this.verifiedBreakpointKeys = []; - this.emit('breakpoints-verified', { + state.isQueued = false; + const breakpoints = this.getBreakpoints(state.pendingRefs); + state.pendingRefs = []; + this.emit(event as Parameters[0], { breakpoints: breakpoints }); }); } } - private verifiedBreakpointKeys: string[] = []; - private isVerifyEventQueued = false; /** - * Generate a key based on the features of the breakpoint. Every breakpoint that exists at the same location - * and has the same features should have the same key. + * Generate a hash based on the features of the breakpoint. Every breakpoint that exists at the same location + * and has the same features should have the same hash. */ - public getBreakpointKey(filePath: string, breakpoint: DebugProtocol.SourceBreakpoint | AugmentedSourceBreakpoint) { + private getBreakpointSrcHash(filePath: string, breakpoint: DebugProtocol.SourceBreakpoint | AugmentedSourceBreakpoint) { const key = `${standardizePath(filePath)}:${breakpoint.line}:${breakpoint.column ?? 0}`; const condition = breakpoint.condition?.trim(); @@ -241,6 +304,30 @@ export class BreakpointManager { return `${key}-standard`; } + /** + * Generate a hash based on the features of the breakpoint. Every breakpoint that exists at the same location + * and has the same features should have the same hash. + */ + private getBreakpointDestHash(breakpoint: BreakpointWorkItem) { + const key = `${standardizePath(breakpoint.stagingFilePath)}:${breakpoint.line}:${breakpoint.column ?? 0}`; + + const condition = breakpoint.condition?.trim(); + if (condition) { + return `${key}-condition=${condition}`; + } + + const hitCondition = parseInt(breakpoint.hitCondition?.trim()); + if (!isNaN(hitCondition)) { + return `${key}-hitCondition=${hitCondition}`; + } + + if (breakpoint.logMessage) { + return `${key}-logMessage=${breakpoint.logMessage}`; + } + + return `${key}-standard`; + } + /** * Set/replace/delete the list of breakpoints for this file. * @param srcPath @@ -310,6 +397,7 @@ export class BreakpointManager { ...breakpoint, //add additional info srcPath: sourceFilePath, + destHash: undefined, rootDirFilePath: s`${project.rootDir}/${relativeStagingPath}`, line: stagingLocation.lineNumber, column: stagingLocation.columnIndex, @@ -318,6 +406,9 @@ export class BreakpointManager { pkgPath: pkgPath, componentLibraryName: (project as ComponentLibraryProject).name }; + obj.destHash = this.getBreakpointDestHash(obj); + obj.deviceId = this.deviceIdByDestHash.get(obj.destHash)?.deviceId; + if (!result[stagingLocation.filePath]) { result[stagingLocation.filePath] = []; } @@ -362,7 +453,7 @@ export class BreakpointManager { promises.push(this.writeBreakpointsToFile(stagingFilePath, breakpoints)); for (const breakpoint of breakpoints) { //mark this breakpoint as verified - this.setBreakpointDeviceId(breakpoint.hash, breakpoint.id); + this.setBreakpointDeviceId(breakpoint.srcHash, breakpoint.destHash, breakpoint.id); this.verifyBreakpoint(breakpoint.id, true); //add this breakpoint to the list of "permanent" breakpoints this.registerPermanentBreakpoint(breakpoint); @@ -545,7 +636,7 @@ export class BreakpointManager { public getPermanentBreakpoint(hash: string) { for (const [, breakpoints] of this.permanentBreakpointsBySrcPath) { for (const breakpoint of breakpoints) { - if (breakpoint.hash === hash) { + if (breakpoint.srcHash === hash) { return breakpoint; } } @@ -599,7 +690,6 @@ export class BreakpointManager { } } - /** * Get a diff of all breakpoints that have changed since the last time the diff was retrieved. * Sets the new baseline to the current state, so the next diff will be based on this new baseline. @@ -637,7 +727,7 @@ export class BreakpointManager { bp.logMessage ].join('--'); //clone the breakpoint and then add it to the current state - currentState.set(key, { ...bp }); + currentState.set(key, { ...bp, deviceId: this.deviceIdByDestHash.get(bp.destHash)?.deviceId }); } } }) @@ -663,15 +753,59 @@ export class BreakpointManager { } } this.lastState = currentState; - return { + + const result = { added: [...added.values()], - removed: [...removed.values()], + removed: [...removed.values(), ...this.failedDeletions], unchanged: [...unchanged.values()] }; + this.failedDeletions = []; + //hydrate the breakpoints with any available deviceIds + for (const breakpoint of [...result.added, ...result.removed, ...result.unchanged]) { + breakpoint.deviceId = this.deviceIdByDestHash.get(breakpoint.destHash)?.deviceId; + } + return result; } finally { this.isGetDiffRunning = false; } } + + /** + * Set the pending status of the given list of breakpoints. + * + * Whenever the breakpoint is currently being handled by an adapter (i.e. add/update/delete), it should + * be marked "pending". Then, when the response comes back (success or fail), "pending" should be set to false. + * In this way, we can ensure that all breakpoints can be synchronized with the device + */ + public setPending(srcPath: string, breakpoints: Breakpoint[], isPending: boolean) { + for (const breakpoint of breakpoints) { + if (breakpoint) { + const hash = this.getBreakpointSrcHash(srcPath, breakpoint); + this.breakpointPendingStatus.set(hash, isPending); + } + } + } + + /** + * Determine whether the current breakpoint is pending or not + */ + public isPending(srcPath: string, breakpoint: Breakpoint); + public isPending(hash: string); + public isPending(...args: [string] | [string, Breakpoint]) { + let hash: string; + if (args[1]) { + hash = this.getBreakpointSrcHash(args[0], args[1]); + } else { + hash = args[0]; + } + return this.breakpointPendingStatus.get(hash) ?? false; + } + + /** + * A map of breakpoint hashes, and whether that breakpoint is currently pending or not. + */ + private breakpointPendingStatus = new Map(); + /** * Flag indicating whether a `getDiff` function is currently running */ @@ -693,11 +827,7 @@ export interface AugmentedSourceBreakpoint extends DebugProtocol.SourceBreakpoin /** * A unique hash generated for the breakpoint at this exact file/line/column/feature. Every breakpoint with these same features should get the same hash */ - hash: string; - /** - * The device-provided breakpoint id. A missing ID means this breakpoint has not yet been verified by the device. - */ - deviceId?: number; + srcHash: string; /** * A unique ID the debug adapter generates to help send updates to the client about this breakpoint */ @@ -740,7 +870,11 @@ export interface BreakpointWorkItem { /** * A unique hash generated for the breakpoint at this exact file/line/column/feature. Every breakpoint with these same features should get the same hash */ - hash: string; + srcHash: string; + /** + * + */ + destHash: string; /** * The 0-based column index */ @@ -768,3 +902,14 @@ export interface BreakpointWorkItem { */ type: 'fileMap' | 'sourceDirs' | 'sourceMap'; } + +export type Breakpoint = DebugProtocol.SourceBreakpoint | AugmentedSourceBreakpoint; + +/** + * A way to reference a breakpoint. + * - `string` - a hash + * - `AugmentedSourceBreakpoint` an actual breakpoint + * - `{hash: string}` - an object containing a breakpoint hash + * - `Breakpoint & {srcPath: string}` - an object with all the properties of a breakpoint _and_ an explicitly defined `srcPath` + */ +export type BreakpointRef = string | AugmentedSourceBreakpoint | { srcHash: string } | (Breakpoint & { srcPath: string }); diff --git a/src/managers/ProjectManager.spec.ts b/src/managers/ProjectManager.spec.ts index 9b7732aa..e86b92ee 100644 --- a/src/managers/ProjectManager.spec.ts +++ b/src/managers/ProjectManager.spec.ts @@ -53,6 +53,10 @@ describe('ProjectManager', () => { }); }); + afterEach(() => { + sinon.restore(); + }); + describe('getLineNumberOffsetByBreakpoints', () => { let filePath = 'does not matter'; it('accounts for the entry breakpoint', () => { @@ -305,6 +309,10 @@ describe('Project', () => { }); }); + afterEach(() => { + sinon.restore(); + }); + it('copies the necessary properties onto the instance', () => { expect(project.rootDir).to.equal(cwd); expect(project.files).to.eql(['a']); diff --git a/src/managers/ProjectManager.ts b/src/managers/ProjectManager.ts index 61910864..a16d7156 100644 --- a/src/managers/ProjectManager.ts +++ b/src/managers/ProjectManager.ts @@ -11,6 +11,7 @@ import { fileUtils, standardizePath as s } from '../FileUtils'; import type { LocationManager, SourceLocation } from './LocationManager'; import { util } from '../util'; import { logger } from '../logging'; +import { Cache } from 'brighterscript/dist/Cache'; // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports const replaceInFile = require('replace-in-file'); @@ -38,6 +39,8 @@ export class ProjectManager { enableDebugProtocol?: boolean; }; + public logger = logger.createLogger('[ProjectManager]'); + public mainProject: Project; public componentLibraryProjects = [] as ComponentLibraryProject[]; @@ -96,50 +99,54 @@ export class ProjectManager { return sourceLineByDebuggerLine[debuggerLineNumber]; } + public sourceLocationCache = new Cache>(); + /** * @param debuggerPath * @param debuggerLineNumber - the 1-based line number from the debugger */ public async getSourceLocation(debuggerPath: string, debuggerLineNumber: number) { - //get source location using - let stagingFileInfo = await this.getStagingFileInfo(debuggerPath); - if (!stagingFileInfo) { - return; - } - let project = stagingFileInfo.project; + return this.sourceLocationCache.getOrAdd(`${debuggerPath}-${debuggerLineNumber}`, async () => { + //get source location using + let stagingFileInfo = await this.getStagingFileInfo(debuggerPath); + if (!stagingFileInfo) { + return; + } + let project = stagingFileInfo.project; - //remove the component library postfix if present - if (project instanceof ComponentLibraryProject) { - stagingFileInfo.absolutePath = fileUtils.unPostfixFilePath(stagingFileInfo.absolutePath, project.postfix); - stagingFileInfo.relativePath = fileUtils.unPostfixFilePath(stagingFileInfo.relativePath, project.postfix); - } + //remove the component library postfix if present + if (project instanceof ComponentLibraryProject) { + stagingFileInfo.absolutePath = fileUtils.unPostfixFilePath(stagingFileInfo.absolutePath, project.postfix); + stagingFileInfo.relativePath = fileUtils.unPostfixFilePath(stagingFileInfo.relativePath, project.postfix); + } - let sourceLocation = await this.locationManager.getSourceLocation({ - lineNumber: debuggerLineNumber, - columnIndex: 0, - fileMappings: project.fileMappings, - rootDir: project.rootDir, - stagingFilePath: stagingFileInfo.absolutePath, - stagingFolderPath: project.stagingFolderPath, - sourceDirs: project.sourceDirs, - enableSourceMaps: this.launchConfiguration?.enableSourceMaps ?? true - }); + let sourceLocation = await this.locationManager.getSourceLocation({ + lineNumber: debuggerLineNumber, + columnIndex: 0, + fileMappings: project.fileMappings, + rootDir: project.rootDir, + stagingFilePath: stagingFileInfo.absolutePath, + stagingFolderPath: project.stagingFolderPath, + sourceDirs: project.sourceDirs, + enableSourceMaps: this.launchConfiguration?.enableSourceMaps ?? true + }); - //if sourcemaps are disabled, and this is a telnet debug dession, account for breakpoint offsets - if (sourceLocation && this.launchConfiguration?.enableSourceMaps === false && !this.launchConfiguration.enableDebugProtocol) { - sourceLocation.lineNumber = this.getLineNumberOffsetByBreakpoints(sourceLocation.filePath, sourceLocation.lineNumber); - } + //if sourcemaps are disabled, and this is a telnet debug dession, account for breakpoint offsets + if (sourceLocation && this.launchConfiguration?.enableSourceMaps === false && !this.launchConfiguration.enableDebugProtocol) { + sourceLocation.lineNumber = this.getLineNumberOffsetByBreakpoints(sourceLocation.filePath, sourceLocation.lineNumber); + } - if (!sourceLocation?.filePath) { - //couldn't find a source location. At least send back the staging file information so the user can still debug - return { - filePath: stagingFileInfo.absolutePath, - lineNumber: sourceLocation?.lineNumber || debuggerLineNumber, - columnIndex: 0 - } as SourceLocation; - } else { - return sourceLocation; - } + if (!sourceLocation?.filePath) { + //couldn't find a source location. At least send back the staging file information so the user can still debug + return { + filePath: stagingFileInfo.absolutePath, + lineNumber: sourceLocation?.lineNumber || debuggerLineNumber, + columnIndex: 0 + } as SourceLocation; + } else { + return sourceLocation; + } + }); } /** @@ -153,6 +160,7 @@ export class ProjectManager { //convert entry point staging location to source location let sourceLocation = await this.getSourceLocation(entryPoint.relativePath, entryPoint.lineNumber); + this.logger.info(`Registering entry breakpoint at ${sourceLocation.filePath}:${sourceLocation.lineNumber} (${entryPoint.pathAbsolute}:${entryPoint.lineNumber})`); //register the entry breakpoint this.breakpointManager.setBreakpoint(sourceLocation.filePath, { //+1 to select the first line of the function diff --git a/src/testHelpers.spec.ts b/src/testHelpers.spec.ts index 69689a3e..512a3fef 100644 --- a/src/testHelpers.spec.ts +++ b/src/testHelpers.spec.ts @@ -1,5 +1,6 @@ import { expect } from 'chai'; import dedent = require('dedent'); +import { SmartBuffer } from 'smart-buffer'; /** * Forces all line endings to \n @@ -62,3 +63,43 @@ export function expectPickEquals(subjects: any[], patterns: any[]) { patterns ); } + +/** + * Build a buffer of `byteCount` size and fill it with random data + */ +export function getRandomBuffer(byteCount: number) { + const result = new SmartBuffer(); + for (let i = 0; i < byteCount; i++) { + result.writeUInt8(i); + } + return result.toBuffer(); +} + +export function expectThrows(callback: () => any, expectedMessage = undefined, failedTestMessage = 'Expected to throw but did not') { + let wasExceptionThrown = false; + try { + callback(); + } catch (e) { + wasExceptionThrown = true; + if (expectedMessage) { + expect(e.message).to.eql(expectedMessage); + } + } + if (wasExceptionThrown === false) { + throw new Error(failedTestMessage); + } +} +export async function expectThrowsAsync(callback: () => any, expectedMessage = undefined, failedTestMessage = 'Expected to throw but did not') { + let wasExceptionThrown = false; + try { + await Promise.resolve(callback()); + } catch (e) { + wasExceptionThrown = true; + if (expectedMessage) { + expect(e.message).to.eql(expectedMessage); + } + } + if (wasExceptionThrown === false) { + throw new Error(failedTestMessage); + } +} diff --git a/src/util.spec.ts b/src/util.spec.ts index d363ca48..f0e5b2c7 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -17,6 +17,27 @@ beforeEach(() => { describe('Util', () => { + describe('hasNonNullishProperty', () => { + it('detects objects with only nullish props or no props at all', () => { + expect(util.hasNonNullishProperty({})).to.be.false; + expect(util.hasNonNullishProperty([])).to.be.false; + expect(util.hasNonNullishProperty(null)).to.be.false; + expect(util.hasNonNullishProperty(undefined)).to.be.false; + expect(util.hasNonNullishProperty(1 as any)).to.be.false; + expect(util.hasNonNullishProperty(true as any)).to.be.false; + expect(util.hasNonNullishProperty(/asdf/)).to.be.false; + expect(util.hasNonNullishProperty({ nullish: null })).to.be.false; + expect(util.hasNonNullishProperty({ nullish: undefined })).to.be.false; + }); + + it('detects objects with defined props', () => { + expect(util.hasNonNullishProperty({ val: true })).to.be.true; + expect(util.hasNonNullishProperty({ val: true, nullish: undefined })).to.be.true; + expect(util.hasNonNullishProperty({ val: false })).to.be.true; + expect(util.hasNonNullishProperty({ val: false, nullish: false })).to.be.true; + }); + }); + describe('isAssignableExpression', () => { it('works', () => { expect(util.isAssignableExpression('function test(): endFunction')).to.be.false; @@ -324,7 +345,7 @@ describe('Util', () => { 1* pkg:/components/MainScene.brs(6) STOP *selected - Brightscript Debugger> + Brightscript Debugger> `)) ).to.eql(dedent` ID Location Source Code @@ -332,7 +353,7 @@ describe('Util', () => { 1* pkg:/components/MainScene.brs(6) STOP *selected - Brightscript Debugger> + Brightscript Debugger> `); }); @@ -343,7 +364,7 @@ describe('Util', () => { 1* pkg:/components/MainScene.brs(6) STOP *selected - Brightscript Debugger> + Brightscript Debugger> `; expect( util.removeThreadAttachedText(text) @@ -353,7 +374,7 @@ describe('Util', () => { it('matches truncated file paths', () => { const text = ` Thread attached: ...Modules/MainMenu/MainMenu.brs(309) renderTracking = m.top.renderTracking - + Brightscript Debugger> `; expect( diff --git a/src/util.ts b/src/util.ts index 636fc8e0..ad8be3d4 100644 --- a/src/util.ts +++ b/src/util.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as fsExtra from 'fs-extra'; import * as net from 'net'; import * as url from 'url'; -import type { SmartBuffer } from 'smart-buffer'; +import * as portfinder from 'portfinder'; import type { BrightScriptDebugSession } from './debugSession/BrightScriptDebugSession'; import { LogOutputEvent } from './debugSession/Events'; import type { AssignmentStatement, Position, Range } from 'brighterscript'; @@ -124,27 +124,6 @@ class Util { }); } - /** - * Tries to read a string from the buffer and will throw an error if there is no null terminator. - * @param {SmartBuffer} bufferReader - */ - public readStringNT(bufferReader: SmartBuffer): string { - // Find next null character (if one is not found, throw) - let buffer = bufferReader.toBuffer(); - let foundNullTerminator = false; - for (let i = bufferReader.readOffset; i < buffer.length; i++) { - if (buffer[i] === 0x00) { - foundNullTerminator = true; - break; - } - } - - if (!foundNullTerminator) { - throw new Error('Could not read buffer string as there is no null terminator.'); - } - return bufferReader.readStringNT(); - } - /** * A reference to the current debug session. Used for logging, and set in the debug session constructor */ @@ -456,6 +435,35 @@ class Util { }); }); } + + /** + * Does the supplied value have at least one defined property with a non-nullish value? + */ + public hasNonNullishProperty(value: Record) { + return Object.values( + value ?? {} + ).some(x => !this.isNullish(x)); + } + + private minPort = 1; + + public async getPort() { + let port: number; + try { + port = await portfinder.getPortPromise({ + //startPort + port: this.minPort + }); + } catch { + this.minPort = 1; + port = await portfinder.getPortPromise({ + //startPort + port: this.minPort + }); + } + this.minPort = port + 1; + return port; + } } export function defer() { @@ -467,6 +475,11 @@ export function defer() { }); return { promise: promise, + tryResolve: function tryResolve(value?: PromiseLike | T) { + if (!this.isCompleted) { + this.resolve(value); + } + }, resolve: function resolve(value?: PromiseLike | T) { if (!this.isResolved) { this.isResolved = true; @@ -479,6 +492,11 @@ export function defer() { ); } }, + tryReject: function tryReject(reason?: any) { + if (!this.isCompleted) { + this.reject(reason); + } + }, reject: function reject(reason?: any) { if (!this.isCompleted) { this.isRejected = true;