diff --git a/.eslintrc.js b/.eslintrc.js index dd567d06..4d28c891 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,11 +20,13 @@ module.exports = { ], rules: { '@typescript-eslint/array-type': 'off', + '@typescript-eslint/class-literal-property-style': 'off', '@typescript-eslint/consistent-type-assertions': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-member-accessibility': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/init-declarations': 'off', + '@typescript-eslint/lines-around-comment': 'off', '@typescript-eslint/lines-between-class-members': 'off', '@typescript-eslint/member-ordering': 'off', '@typescript-eslint/method-signature-style': 'off', @@ -51,12 +53,16 @@ module.exports = { '@typescript-eslint/no-unnecessary-condition': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-enum-comparison': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/no-unused-vars-experimental': 'off', '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/object-curly-spacing': ['error', 'always'], + '@typescript-eslint/parameter-properties': 'off', + '@typescript-eslint/prefer-nullish-coalescing': 'off', + '@typescript-eslint/prefer-optional-chain': 'off', '@typescript-eslint/prefer-readonly': 'off', '@typescript-eslint/prefer-readonly-parameter-types': 'off', '@typescript-eslint/promise-function-async': 'off', @@ -70,6 +76,7 @@ module.exports = { '@typescript-eslint/require-array-sort-compare': 'off', '@typescript-eslint/restrict-plus-operands': 'off', '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/sort-type-constituents': 'off', '@typescript-eslint/sort-type-union-intersection-members': 'off', '@typescript-eslint/space-before-function-paren': 'off', '@typescript-eslint/strict-boolean-expressions': 'off', @@ -120,6 +127,7 @@ module.exports = { 'no-constant-condition': 'off', 'no-console': 'off', 'no-continue': 'off', + 'no-duplicate-imports': 'off', 'no-else-return': 'off', 'no-empty': 'off', 'no-implicit-coercion': 'off', diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 377bcad7..7cb39674 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,11 @@ name: build -on: [push, pull_request] +on: + push: + branches: + - master + tags: + - v* + pull_request: jobs: ci: @@ -9,29 +15,31 @@ jobs: COVERALLS_REPO_TOKEN: "NMGk1IhVG2Ds5VQKiEuXpZE8xftkORa7W" strategy: matrix: - os: [ubuntu-18.04, macos-10.15, windows-2019] + os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@master - uses: actions/setup-node@master with: - node-version: "12.22.7" + node-version: "14.18.1" + architecture: 'x64' # fix for macos-latest - run: npm ci - run: npm run build - run: npm run lint - run: npm run test - - run: npm run publish-coverage + #- 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) if: startsWith(github.ref, 'refs/tags/v') needs: ci - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - uses: actions/checkout@master - uses: actions/setup-node@master with: - node-version: "10.19.0" + node-version: "14.18.1" + architecture: 'x64' # fix for macos-latest - run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ./.npmrc - run: npm ci - run: npm run build diff --git a/.github/workflows/create-package.yml b/.github/workflows/create-package.yml new file mode 100644 index 00000000..4f130832 --- /dev/null +++ b/.github/workflows/create-package.yml @@ -0,0 +1,57 @@ +name: create-package +on: + pull_request: + types: [labeled, unlabeled, synchronize] +jobs: + create-package: + runs-on: ubuntu-latest + if: contains(github.event.pull_request.labels.*.name, 'create-package') + env: + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@master + - uses: actions/setup-node@master + with: + node-version: "14.19.0" + # Get a bot token so the bot's name shows up on all our actions + - name: Get Token From roku-ci-token Application + uses: tibdex/github-app-token@v1 + id: generate-token + with: + app_id: ${{ secrets.BOT_APP_ID }} + private_key: ${{ secrets.BOT_PRIVATE_KEY }} + - run: echo "TOKEN=${{ steps.generate-token.outputs.token }}" >> $GITHUB_ENV + - name: Compute variables + run: | + CURRENT_VERSION=$(grep -o '\"version\": *\"[^\"]*\"' package.json | awk -F'\"' '{print $4}') + SANITIZED_BRANCH_NAME=$(echo "$GITHUB_HEAD_REF" | sed 's/[^0-9a-zA-Z-]/-/g') + BUILD_VERSION="$CURRENT_VERSION-$SANITIZED_BRANCH_NAME.$(date +%Y%m%d%H%M%S)" + NPM_PACKAGE_NAME=$(grep -o '\"name\": *\"[^\"]*\"' package.json | awk -F'\"' '{print $4}') + ARTIFACT_NAME=$(echo "$NPM_PACKAGE_NAME-$BUILD_VERSION.tgz" | tr '/' '-') + ARTIFACT_URL="${{ github.server_url }}/${{ github.repository }}/releases/download/v0.0.0-packages/${ARTIFACT_NAME}" + + echo "BUILD_VERSION=$BUILD_VERSION" >> $GITHUB_ENV + echo "ARTIFACT_URL=$ARTIFACT_URL" >> $GITHUB_ENV + + - run: npm ci + - run: npm version "$BUILD_VERSION" --no-git-tag-version + - run: npm pack + + # create the release if not exist + - run: gh release create v0.0.0-packages --latest=false --prerelease --notes "catchall release for temp packages" -R ${{ github.repository }} + continue-on-error: true + + # upload this artifact to the "packages" github release + - run: gh release upload v0.0.0-packages *.tgz -R ${{ github.repository }} + + - name: Fetch build artifact + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + return github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "Hey there! I just built a new temporary npm package based on ${{ github.event.pull_request.head.sha }}. You can download it [here](${{ env.ARTIFACT_URL }}) or install it by running the following command: \n```bash\nnpm install ${{ env.ARTIFACT_URL }}\n```" + }); diff --git a/.github/workflows/create-vsix.yml b/.github/workflows/create-vsix.yml index 28f67112..f0afd6e2 100644 --- a/.github/workflows/create-vsix.yml +++ b/.github/workflows/create-vsix.yml @@ -38,5 +38,5 @@ jobs: issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: 'Hey there! I just built a new version of the vscode extension based on ${{ github.event.pull_request.head.sha }}. You can download the .vsix [here](${{steps.create-vsix.outputs.workflow-url}}) and then follow [these installation instructions](https://github.com/rokucommunity/vscode-brightscript-language#pre-release-versions).' + body: 'Hey there! I just built a new version of the vscode extension based on ${{ github.event.pull_request.head.sha }}. You can download the .vsix [here](${{steps.create-vsix.outputs.workflow-url}}) and then follow [these installation instructions](https://rokucommunity.github.io/vscode-brightscript-language/prerelease-versions.html).' }) diff --git a/.vscode/launch.json b/.vscode/launch.json index 11b3c433..0ca8a376 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,6 @@ ], "program": "${workspaceFolder}/dist/index.js", "preLaunchTask": "tsc: build - tsconfig.json", - "protocol": "inspector", "internalConsoleOptions": "openOnSessionStart", "outFiles": [ "${workspaceFolder}/dist/**/*.js" @@ -32,8 +31,7 @@ "500000" ], "cwd": "${workspaceRoot}", - "protocol": "inspector", "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/CHANGELOG.md b/CHANGELOG.md index 24c0d148..b92c2133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,490 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 -## [0.11.0](https://github.com/rokucommunity/roku-debug/compare/v0.10.5...v0.11.0) - 2022-05-05 +## [0.21.9](https://github.com/rokucommunity/roku-debug/compare/v0.21.8...v0.21.9) - 2024-06-03 ### Changed - - upgrade to [brighterscript@0.49.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0490---2022-05-02) + - upgrade to [brighterscript@0.67.2](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0672---2024-06-03) +### Fixed + - Prevent corrupted breakpoints due to invalid sourceDirs, add more logging ([#189](https://github.com/rokucommunity/roku-debug/pull/189)) + + + +## [0.21.8](https://github.com/rokucommunity/roku-debug/compare/v0.21.7...v0.21.8) - 2024-05-16 +### Changed + - upgrade to [brighterscript@0.67.1](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0671---2024-05-16). Notable changes since 0.65.27: + - Fix crash when diagnostic is missing range ([brighterscript#1174](https://github.com/rokucommunity/brighterscript/pull/1174)) + - Upgrade to @rokucommunity/logger ([brighterscript#1137](https://github.com/rokucommunity/brighterscript/pull/1137)) + - upgrade to [@rokucommunity/logger@0.3.9](https://github.com/rokucommunity/logger/blob/master/CHANGELOG.md#039---2024-05-09) +### Fixed + - node14 CI bugs ([#188](https://github.com/rokucommunity/roku-debug/pull/188)) + + + +## [0.21.7](https://github.com/rokucommunity/roku-debug/compare/v0.21.6...v0.21.7) - 2024-03-27 +### Changed + - upgrade to [brighterscript@0.65.27](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#06527---2024-03-27). Notable changes since 0.65.25: + - Upgade LSP packages ([brighterscript#1117](https://github.com/rokucommunity/brighterscript/pull/1117)) + - Increase max param count to 63 ([brighterscript#1112](https://github.com/rokucommunity/brighterscript/pull/1112)) + - Prevent unused variable warnings on ternary and null coalescence expressions ([brighterscript#1101](https://github.com/rokucommunity/brighterscript/pull/1101)) +### Fixed + - Optional Chainging Operator errors in debug console ([#187](https://github.com/rokucommunity/roku-debug/pull/187)) + + + +## [0.21.6](https://github.com/rokucommunity/roku-debug/compare/v0.21.5...v0.21.6) - 2024-03-07 +### Changed + - upgrade to [brighterscript@0.65.25](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#06525---2024-03-07). Notable changes since 0.65.23: + - Support when tokens have null ranges ([brighterscript#1072](https://github.com/rokucommunity/brighterscript/pull/1072)) + - Support whitespace in conditional compile keywords ([brighterscript#1090](https://github.com/rokucommunity/brighterscript/pull/1090)) + + + +## [0.21.5](https://github.com/rokucommunity/roku-debug/compare/v0.21.4...v0.21.5) - 2024-03-01 +### Changed + - Add some enhanced launch settings to support more diverse projects ([#184](https://github.com/rokucommunity/roku-debug/pull/184)) + - upgrade to [roku-deploy@3.12.0](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#3120---2024-03-01). Notable changes since 3.11.3: + - Support overriding various package upload form data ([roku-deploy#136](https://github.com/rokucommunity/roku-deploy/pull/136)) + + + +## [0.21.4](https://github.com/rokucommunity/roku-debug/compare/v0.21.3...v0.21.4) - 2024-02-29 +### Changed + - upgrade to [brighterscript@0.65.23](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#06523---2024-02-29). Notable changes since 0.65.19: + - Fix sourcemap comment and add `file` prop to map ([brighterscript#1064](https://github.com/rokucommunity/brighterscript/pull/1064)) + - Move `coveralls-next` to a devDependency since it's not needed at runtime ([brighterscript#1051](https://github.com/rokucommunity/brighterscript/pull/1051)) + - Fix parsing issues with multi-index IndexedSet and IndexedGet ([brighterscript#1050](https://github.com/rokucommunity/brighterscript/pull/1050)) + - upgrade to [roku-deploy@3.11.3](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#3113---2024-02-29). Notable changes since 3.11.2: + - Retry the convertToSquahsfs request given the HPE_INVALID_CONSTANT error ([roku-deploy#145](https://github.com/rokucommunity/roku-deploy/pull/145)) +### Fixed + - DebugProtocol fixes ([#186](https://github.com/rokucommunity/roku-debug/pull/186)) + - Support relaunch debug protocol ([#181](https://github.com/rokucommunity/roku-debug/pull/181)) + + + +## [0.21.3](https://github.com/rokucommunity/roku-debug/compare/v0.21.2...v0.21.3) - 2024-01-30 +### Changed + - upgrade to [brighterscript@0.65.19](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#06519---2024-01-30). Notable changes since 0.65.18: + - Backport v1 syntax changes ([brighterscript#1034](https://github.com/rokucommunity/brighterscript/pull/1034)) + + + +## [0.21.2](https://github.com/rokucommunity/roku-debug/compare/v0.21.1...v0.21.2) - 2024-01-25 +### Changed + - Use `stagingDir` instead of stagingFolderPath ([#185](https://github.com/rokucommunity/roku-debug/pull/185)) + - upgrade to [brighterscript@0.65.18](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#06518---2024-01-25). Notable changes since 0.65.17: + - Refactor bsconfig documentation ([brighterscript#1024](https://github.com/rokucommunity/brighterscript/pull/1024)) + - Prevent overwriting the Program._manifest if already set on startup ([brighterscript#1027](https://github.com/rokucommunity/brighterscript/pull/1027)) + - Improving null safety: Add FinalizedBsConfig and tweak plugin events ([brighterscript#1000](https://github.com/rokucommunity/brighterscript/pull/1000)) + + + +## [0.21.1](https://github.com/rokucommunity/roku-debug/compare/v0.21.0...v0.21.1) - 2024-01-16 +### Changed + - upgrade to [brighterscript@0.65.17](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#06517---2024-01-16). Notable changes since 0.65.16: + - adds support for libpkg prefix ([brighterscript#1017](https://github.com/rokucommunity/brighterscript/pull/1017)) + - Assign .program to the builder BEFORE calling afterProgram ([brighterscript#1011](https://github.com/rokucommunity/brighterscript/pull/1011)) + + + +## [0.21.0](https://github.com/rokucommunity/roku-debug/compare/v0.20.15...v0.21.0) - 2024-01-10 +### Added + - Add cli flag to run dap as standalone process ([#173](https://github.com/rokucommunity/roku-debug/pull/173)) + - Expose `controlPort` launch option for overriding the debug protocol port ([#182](https://github.com/rokucommunity/roku-debug/pull/182)) + + + +## [0.20.15](https://github.com/rokucommunity/roku-debug/compare/v0.20.14...v0.20.15) - 2024-01-08 +### Changed + - Display a modal message when the we fail to upload a package to the device ([#178](https://github.com/rokucommunity/roku-debug/pull/178)) + - upgrade to [brighterscript@0.65.16](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#06516---2024-01-08) + - upgrade to [roku-deploy@3.11.2](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#3112---2023-12-20). Notable changes since 3.11.1: + - Update wrong host password error message ([roku-deploy#134](https://github.com/rokucommunity/roku-deploy/pull/134)) + + + +## [0.20.14](https://github.com/rokucommunity/roku-debug/compare/v0.20.13...v0.20.14) - 2023-12-07 +### Changed + - make the connection port for SceneGraphDebugCommandController configurable ([#177](https://github.com/rokucommunity/roku-debug/pull/177)) + - upgrade to [brighterscript@0.65.12](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#06512---2023-12-07) + - upgrade to [roku-deploy@3.11.1](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#3111---2023-11-30). Notable changes since 3.10.5: + - wait for file stream to close before resolving promise ([roku-deploy#133](https://github.com/rokucommunity/roku-deploy/pull/133)) + - add public function to normalize device-info field values ([roku-deploy#129](https://github.com/rokucommunity/roku-deploy/pull/129)) + + + +## [0.20.13](https://github.com/rokucommunity/roku-debug/compare/v0.20.12...v0.20.13) - 2023-11-16 +### Fixed + - Fix bug with compile error reporting ([#174](https://github.com/rokucommunity/roku-debug/pull/174)) + + + +## [0.20.12](https://github.com/rokucommunity/roku-debug/compare/v0.20.11...v0.20.12) - 2023-11-14 +### Changed + - Add timeout for deviceinfo query so we don't wait too long ([#171](https://github.com/rokucommunity/roku-debug/pull/171)) + - upgrade to [brighterscript@0.65.10](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#06510---2023-11-14). Notable changes since 0.65.9: + - upgrade to [roku-deploy@3.10.5](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#3105---2023-11-14). Notable changes since 3.10.4: + - better error detection when sideload fails ([roku-deploy#127](https://github.com/rokucommunity/roku-deploy/pull/127)) + + + +## [0.20.11](https://github.com/rokucommunity/roku-debug/compare/v0.20.10...v0.20.11) - 2023-11-11 +### Changed + - Update DebugProtocolClient supported version range ([#170](https://github.com/rokucommunity/roku-debug/pull/170)) + - fix small typo in debug potocol message ([#169](https://github.com/rokucommunity/roku-debug/pull/169)) + + + +## [0.20.10](https://github.com/rokucommunity/roku-debug/compare/v0.20.9...v0.20.10) - 2023-11-08 +### Changed + - upgrade to [brighterscript@0.65.9](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0659---2023-11-06). Notable changes since 0.65.8: + - Fix issue with unary expression parsing ([brighterscript#938](https://github.com/rokucommunity/brighterscript/pull/938)) + - ci: Don't run `test-related-projects` on release since it already ran on build ([#brighterscript157fc2e](https://github.com/rokucommunity/brighterscript/commit/157fc2e)) +### Fixed + - Fix sideload crash related to failed dev app deletion ([#168](https://github.com/rokucommunity/roku-debug/pull/168)) + + + +## [0.20.9](https://github.com/rokucommunity/roku-debug/compare/v0.20.8...v0.20.9) - 2023-11-05 +### Changed + - Upgrade to enhanced deviceInfo api from roku-deploy ([#167](https://github.com/rokucommunity/roku-debug/pull/167)) + - upgrade to [roku-deploy@3.10.4](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#3104---2023-11-03). Notable changes since 3.10.3: + - Enhance getDeviceInfo() method ([roku-deploy#120](https://github.com/rokucommunity/roku-deploy/pull/120)) + + + +## [0.20.8](https://github.com/rokucommunity/roku-debug/compare/v0.20.7...v0.20.8) - 2023-10-31 +### Fixed + - Clean up control socket when it's closed ([#166](https://github.com/rokucommunity/roku-debug/pull/166)) + + + +## [0.20.7](https://github.com/rokucommunity/roku-debug/compare/v0.20.6...v0.20.7) - 2023-10-16 +### Changed + - Debug Protocol Enhancements ([#107](https://github.com/rokucommunity/roku-debug/pull/107)) + + + +## [0.20.6](https://github.com/rokucommunity/roku-debug/compare/v0.20.5...v0.20.6) - 2023-10-06 +### Changed + - upgrade to [brighterscript@0.65.8](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0658---2023-10-06). Notable changes since 0.65.7: + - Bump postcss from 8.2.15 to 8.4.31 ([brighterscript#928](https://github.com/rokucommunity/brighterscript/pull/928)) + - Add interface parameter support ([brighterscript#924](https://github.com/rokucommunity/brighterscript/pull/924)) + - Better typing for `Deferred` ([brighterscript#923](https://github.com/rokucommunity/brighterscript/pull/923)) +### Fixed + - bug with telnet getting stuck ([#163](https://github.com/rokucommunity/roku-debug/pull/163)) + + + +## [0.20.5](https://github.com/rokucommunity/roku-debug/compare/v0.20.4...v0.20.5) - 2023-09-28 +### Changed + - upgrade to [brighterscript@0.65.7](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0657---2023-09-28). Notable changes since 0.65.5: + + + +## [0.20.4](https://github.com/rokucommunity/roku-debug/compare/v0.20.3...v0.20.4) - 2023-09-11 +### Changed + - upgrade to [brighterscript@0.65.5](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0655---2023-09-06). Notable changes since 0.65.4: + - Fix crashes in util for null ranges ([brighterscript#869](https://github.com/rokucommunity/brighterscript/pull/869)) + + + +## [0.20.3](https://github.com/rokucommunity/roku-debug/compare/v0.20.2...0.20.3) - 2023-07-26 +### Added + - Add `deleteDevChannelBeforeInstall` launch option ([#158](https://github.com/rokucommunity/roku-debug/pull/158)) + + + +## [0.20.2](https://github.com/rokucommunity/roku-debug/compare/v0.20.1...v0.20.2) - 2023-07-24 +### Changed + - Bump word-wrap from 1.2.3 to 1.2.4 ([#157](https://github.com/rokucommunity/roku-debug/pull/157)) + - upgrade to [brighterscript@0.65.4](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0654---2023-07-24). Notable changes since 0.65.1: + - Bump word-wrap from 1.2.3 to 1.2.4 ([brighterscript#851](https://github.com/rokucommunity/brighterscript/pull/851)) + - Bump semver from 6.3.0 to 6.3.1 in /benchmarks ([brighterscript#838](https://github.com/rokucommunity/brighterscript/pull/838)) + - Bump semver from 5.7.1 to 5.7.2 ([brighterscript#837](https://github.com/rokucommunity/brighterscript/pull/837)) + - Prevent crashing when diagnostic is missing range. ([brighterscript#832](https://github.com/rokucommunity/brighterscript/pull/832)) + - Prevent crash when diagnostic is missing range ([brighterscript#831](https://github.com/rokucommunity/brighterscript/pull/831)) + - upgrade to [roku-deploy@3.10.3](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#3103---2023-07-22). Notable changes since 3.10.2: + - Bump word-wrap from 1.2.3 to 1.2.4 ([roku-deploy#117](https://github.com/rokucommunity/roku-deploy/pull/117)) + + + +## [0.20.1](https://github.com/rokucommunity/roku-debug/compare/v0.20.0...v0.20.1) - 2023-07-07 +### Changed + - Fix rendezvous crash ([#156](https://github.com/rokucommunity/roku-debug/pull/156)) + + + +## [0.20.0](https://github.com/rokucommunity/roku-debug/compare/v0.19.1...v0.20.0) - 2023-07-05 +### Added + - Support sgrendezvous through ECP ([#150](https://github.com/rokucommunity/roku-debug/pull/150)) +### Changed + - upgrade to [brighterscript@0.65.1](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0651---2023-06-09) + + + +## [0.19.1](https://github.com/rokucommunity/roku-debug/compare/v0.19.0...v0.19.1) - 2023-06-08 +### Changed + - Move @types/request to deps to fix d.bs files ([691a7be](https://github.com/rokucommunity/roku-debug/commit/691a7be)) + + + +## [0.19.0](https://github.com/rokucommunity/roku-debug/compare/v0.18.12...v0.19.0) - 2023-06-01 +### Added + - File logging ([#155](https://github.com/rokucommunity/roku-debug/pull/155)) + + + +## [0.18.12](https://github.com/rokucommunity/roku-debug/compare/v0.18.11...v0.18.12) - 2023-05-18 +### Changed + - remove axios in favor of postman-request ([#153](https://github.com/rokucommunity/roku-debug/pull/153)) +### Fixed + - Fix `file already exists` error and hung process ([#152](https://github.com/rokucommunity/roku-debug/pull/152)) + + + +## [0.18.11](https://github.com/rokucommunity/roku-debug/compare/v0.18.10...v0.18.11) - 2023-05-17 +### Changed + - Fix crash by using postman-request ([#151](https://github.com/rokucommunity/roku-debug/pull/151)) + + + +## [0.18.10](https://github.com/rokucommunity/roku-debug/compare/v0.18.9...v0.18.10) - 2023-05-17 +### Changed + - upgrade to [brighterscript@0.65.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0650---2023-05-17) + - upgrade to [@rokucommunity/logger@0.3.3](https://github.com/rokucommunity/logger/blob/master/CHANGELOG.md#033---2023-05-17). Notable changes since 0.3.2: + - Fix dependencies ([#@rokucommunity/logger04af7a0](https://github.com/rokucommunity/logger/commit/04af7a0)) + + + +## [0.18.9](https://github.com/rokucommunity/roku-debug/compare/v0.18.8...v0.18.9) - 2023-05-10 +### Changed + - upgrade to [brighterscript@0.64.4](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0644---2023-05-10) + - upgrade to [roku-deploy@3.10.2](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#3102---2023-05-10). Notable changes since 3.10.1: + - Fix audit issues ([roku-deploy#116](https://github.com/rokucommunity/roku-deploy/pull/116)) + - fix nodejs 19 bug ([roku-deploy#115](https://github.com/rokucommunity/roku-deploy/pull/115)) + + + +## [0.18.8](https://github.com/rokucommunity/roku-debug/compare/v0.18.7...v0.18.8) - 2023-04-28 +### Changed + - Make axios a prod dependency ([#148](https://github.com/rokucommunity/roku-debug/pull/148)) + + + +## [0.18.7](https://github.com/rokucommunity/roku-debug/compare/v0.18.6...v0.18.7) - 2023-04-28 +### Added + - better error for failed session starts ([#147](https://github.com/rokucommunity/roku-debug/pull/147)) + - adds device-info query results to debug session ([#130](https://github.com/rokucommunity/roku-debug/pull/130)) +### Changed + - Bump xml2js from 0.4.23 to 0.5.0 ([#146](https://github.com/rokucommunity/roku-debug/pull/146)) + - upgrade to [brighterscript@0.64.3](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0643---2023-04-28). Notable changes since 0.64.2: + - Improves performance in symbol table fetching ([brighterscript#797](https://github.com/rokucommunity/brighterscript/pull/797)) + + + +## [0.18.6](https://github.com/rokucommunity/roku-debug/compare/v0.18.5...v0.18.6) - 2023-04-18 +### Changed + - Exclude sourcemaps when sideloading ([#145](https://github.com/rokucommunity/roku-debug/pull/145)) + - upgrade to [brighterscript@0.64.2](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0642---2023-04-18). Notable changes since 0.64.1: + - Fix namespace-relative enum value ([brighterscript#793](https://github.com/rokucommunity/brighterscript/pull/793)) + + + +## [0.18.5](https://github.com/rokucommunity/roku-debug/compare/v0.18.4...v0.18.5) - 2023-04-14 +### Changed + - upgrade to [brighterscript@0.64.1](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0641---2023-04-14). Notable changes since 0.62.0: + - Bump xml2js from 0.4.23 to 0.5.0 ([brighterscript#790](https://github.com/rokucommunity/brighterscript/pull/790)) + - upgrade to [roku-deploy@3.10.1](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#3101---2023-04-14). Notable changes since 3.10.0: + - Bump xml2js from 0.4.23 to 0.5.0 ([roku-deploy#112](https://github.com/rokucommunity/roku-deploy/pull/112)) + + + +## [0.18.4](https://github.com/rokucommunity/roku-debug/compare/v0.18.3...v0.18.4) - 2023-03-17 +### Changed + - upgrade to [brighterscript@0.62.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0620---2023-03-17). Notable changes since 0.61.3: + - Fix crash when func has no block ([brighterscript#774](https://github.com/rokucommunity/brighterscript/pull/774)) + - Move not-referenced check into ProgramValidator ([brighterscript#773](https://github.com/rokucommunity/brighterscript/pull/773)) + - upgrade to [@rokucommunity/logger@0.3.2](https://github.com/rokucommunity/logger/blob/master/CHANGELOG.md#032---2023-03-16). Notable changes since 0.3.1: + - Fix crash when encountering bigint ([@rokucommunity/logger#3](https://github.com/rokucommunity/logger/pull/3)) + - upgrade to [roku-deploy@3.10.0](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#3100---2023-03-16). Notable changes since 3.9.3: + - Use micromatch instead of picomatch ([roku-deploy#109](https://github.com/rokucommunity/roku-deploy/pull/109)) + + + +## [0.18.3](https://github.com/rokucommunity/roku-debug/compare/v0.18.2...v0.18.3) - 2023-01-31 +### Fixed + - Increase the timeout for debug protocol control to prevent timeout with large projects ([#134](https://github.com/rokucommunity/roku-debug/pull/134)) + + + +## [0.18.2](https://github.com/rokucommunity/roku-debug/compare/v0.18.1...v0.18.2) - 2023-01-27 +### Fixed + - off-by-1 bug with threads over protocol ([#132](https://github.com/rokucommunity/roku-debug/pull/132)) + + + +## [0.18.1](https://github.com/rokucommunity/roku-debug/compare/v0.18.0...v0.18.1) - 2023-01-24 +### Changed + - Hide debugger-created temp variables from the variables panel, add `showHiddenVariables` flag to disable it if desired. ([#127](https://github.com/rokucommunity/roku-debug/pull/127)) + - upgrade to [brighterscript@0.61.3](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0613---2023-01-12). Notable changes since 0.61.2: + - upgrade to [@rokucommunity/logger@0.3.1](https://github.com/rokucommunity/logger/blob/master/CHANGELOG.md#031---2023-01-24). Notable changes since 0.3.0: +### Fixed + - `isAssignableExpression` to correctly support `DottedSet` and `IndexedSet` statements ([#128](https://github.com/rokucommunity/roku-debug/pull/128)) + + + +## [0.18.0](https://github.com/rokucommunity/roku-debug/compare/v0.17.3...v0.18.0) - 2023-01-12 +### Added + - Execute command for repl expressions ([#119](https://github.com/rokucommunity/roku-debug/pull/119)) +### Changed + - upgrade to [roku-deploy@3.9.3](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#393---2023-01-12) +### Fixed + - inifinite spin for unloaded vars ([#120](https://github.com/rokucommunity/roku-debug/pull/120)) + + + +## [0.17.3](https://github.com/rokucommunity/roku-debug/compare/v0.17.2...v0.17.3) - 2022-12-15 +### Added + - Debug protocol breakpoint verification ([#117](https://github.com/rokucommunity/roku-debug/pull/117)) + + + +## [0.17.2](https://github.com/rokucommunity/roku-debug/compare/v0.17.1...v0.17.2) - 2022-12-15 +### Changed + - upgrade to [brighterscript@0.61.2](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0612---2022-12-15). Notable changes since 0.61.1: + - Bump qs from 6.5.2 to 6.5.3 ([brighterscript#758](https://github.com/rokucommunity/brighterscript/pull/758)) + + + +## [0.17.1](https://github.com/rokucommunity/roku-debug/compare/v0.17.0...v0.17.1) - 2022-12-08 +### Fixed + - Fix "continue" repeat bug in protocol adapter ([#114](https://github.com/rokucommunity/roku-debug/pull/114)) + - Fix issue with truncated debugger paths ([#113](https://github.com/rokucommunity/roku-debug/pull/113)) + - Bugfix/do not alter `outFilePath` for libraries ([#112](https://github.com/rokucommunity/roku-debug/pull/112)) + - upgrade to [brighterscript@0.61.1](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0611---2022-12-07) + + + +## [0.17.0](https://github.com/rokucommunity/roku-debug/compare/v0.16.1...v0.17.0) - 2022-11-02 +### Changed + - Added the `brightscript_warnings` command ([#110](https://github.com/rokucommunity/roku-debug/pull/110)) + + + +## [0.16.1](https://github.com/rokucommunity/roku-debug/compare/v0.16.0...v0.16.1) - 2022-10-28 +### Changed + - upgrade to [brighterscript@0.60.4](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0604---2022-10-28). Notable changes since 0.60.0: + - Allow `continue` as local var ([brighterscript#730](https://github.com/rokucommunity/brighterscript/pull/730)) + - better parse recover for unknown func params ([brighterscript#722](https://github.com/rokucommunity/brighterscript/pull/722)) + - Fix if statement block var bug ([brighterscript#698](https://github.com/rokucommunity/brighterscript/pull/698)) + + + +## [0.16.0](https://github.com/rokucommunity/roku-debug/compare/v0.15.0...v0.16.0) - 2022-10-17 +### Changed + - Emit device diagnostics instead of compile errors ([#104](https://github.com/rokucommunity/roku-debug/pull/104)) + - Standardize custom events, add is* helpers ([#103](https://github.com/rokucommunity/roku-debug/pull/103)) + - upgrade to [brighterscript@0.60.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0600---2022-10-10). Notable changes since 0.56.0: + - upgrade to [roku-deploy@3.9.2](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#392---2022-10-03). Notable changes since 3.7.1: + - Replace minimatch with picomatch ([roku-deploy#101](https://github.com/rokucommunity/roku-deploy/pull/101)) + - Sync retainStagingFolder, stagingFolderPath with options. ([roku-deploy#100](https://github.com/rokucommunity/roku-deploy/pull/100)) + - Add stagingDir and retainStagingDir. ([roku-deploy#99](https://github.com/rokucommunity/roku-deploy/pull/99)) + - Remotedebug connect early ([roku-deploy#97](https://github.com/rokucommunity/roku-deploy/pull/97)) + - Better compile error handling ([roku-deploy#96](https://github.com/rokucommunity/roku-deploy/pull/96)) +### Fixed + - crash in rendezvous parser for missing files ([#108](https://github.com/rokucommunity/roku-debug/pull/108)) + - better debug protocol launch handling ([#102](https://github.com/rokucommunity/roku-debug/pull/102)) + + + +## [0.15.0](https://github.com/rokucommunity/roku-debug/compare/v0.14.2...v0.15.0) - 2022-08-23 +### Added + - support for conditional breakpoints over the debug protocol([#97](https://github.com/rokucommunity/roku-debug/pull/97)) +### Changed +- upgrade to [brighterscript@0.56.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0560---2022-08-23). Notable changes since 0.55.1: + - Fix compile crash for scope-less files ([brighterscript#674](https://github.com/rokucommunity/brighterscript/pull/674)) + - Allow const as variable name ([brighterscript#670](https://github.com/rokucommunity/brighterscript/pull/670)) +### Fixed + - `stopOnEntry` bug with `deepLinkUrl`. ([#100](https://github.com/rokucommunity/roku-debug/pull/100)) + - bug that was omitting `invalid` data types over the debug protocol ([#99](https://github.com/rokucommunity/roku-debug/pull/99)) + + + +## [0.14.2](https://github.com/rokucommunity/roku-debug/compare/v0.14.1...v0.14.2) - 2022-08-12 +### Changed + - Support complib breakpoints on 11.5.0 ([#96](https://github.com/rokucommunity/roku-debug/pull/96)) + - Disable thread hopping workaround >= protocol v3.1.0 ([#95](https://github.com/rokucommunity/roku-debug/pull/95)) + - Upload zip and connect to protocol socket in parallel ([#94](https://github.com/rokucommunity/roku-debug/pull/94)) + - upgrade to [brighterscript@0.55.1](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0551---2022-08-07). Notable changes since 0.53.1: + - Fix typescript error for ast parent setting ([brighterscript#659](https://github.com/rokucommunity/brighterscript/pull/659)) + - Performance boost: better function sorting during validation ([brighterscript#651](https://github.com/rokucommunity/brighterscript/pull/651)) + - Export some vscode interfaces ([brighterscript#644](https://github.com/rokucommunity/brighterscript/pull/644)) + + + +## [0.14.1](https://github.com/rokucommunity/roku-debug/compare/v0.14.0...v0.14.1) - 2022-07-16 +### Changed + - Bump moment from 2.29.2 to 2.29.4 ([#92](https://github.com/rokucommunity/roku-debug/pull/92)) + - upgrade to [brighterscript@0.53.1](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0531---2022-07-15). Notable changes since 0.53.0: + - Bump moment from 2.29.2 to 2.29.4 ([brighterscript#640](https://github.com/rokucommunity/brighterscript/pull/640)) + + + +## [0.14.0](https://github.com/rokucommunity/roku-debug/compare/v0.13.1...v0.14.0) - 2022-07-14 +### Added + - debug protocol: support for case-sensitivity in getVariables protocol request ([#91](https://github.com/rokucommunity/roku-debug/pull/91)) + - Show error when cannot resolve hostname ([#90](https://github.com/rokucommunity/roku-debug/pull/90)) +### Changed + - upgrade to [brighterscript@0.53.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0530---2022-07-14) + + + +## [0.13.1](https://github.com/rokucommunity/roku-debug/compare/v0.13.0...v0.13.1) - 2022-06-09 +### Fixed + - dynamic breakpoints bug where component library breakpoints weren't being hit ([#89](https://github.com/rokucommunity/roku-debug/pull/89)) + + + +## [0.13.0](https://github.com/rokucommunity/roku-debug/compare/v0.12.2...v0.13.0) - 2022-06-08 +### Added + - Support for dynamic breakpoints when using Debug Protocol ([#84](https://github.com/rokucommunity/roku-debug/pull/84)) +### Changed + - upgrade to [brighterscript@0.52.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0520---2022-06-08) + - upgrade to [roku-deploy@3.7.1](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#371---2022-06-08) +### Fixed + - crash when RAF files show up in stacktrace ([#88](https://github.com/rokucommunity/roku-debug/pull/88)) + + + +## [0.12.2](https://github.com/rokucommunity/roku-debug/compare/v0.12.1...v0.12.2) - 2022-05-31 +### Changed + - upgrade to [brighterscript@0.51.3](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0513---2022-05-31) + - upgrade to [roku-deploy@3.7.0](https://github.com/rokucommunity/roku-deploy/blob/master/CHANGELOG.md#370---2022-05-23) +### Fixed + - line number and thread hopping fixes ([#86](https://github.com/rokucommunity/roku-debug/pull/86)) + + + +## [0.12.1](https://github.com/rokucommunity/roku-debug/compare/v0.12.0...v0.12.1) - 2022-05-20 +### Changed + - add `launchConfiguration` to the `ChannelPublishedEvent` ([#83](https://github.com/rokucommunity/roku-debug/pull/83)) + ### Fixed + - crash during rendezvous tracking ([#82](https://github.com/rokucommunity/roku-debug/pull/82)) + + + +## [0.12.0](https://github.com/rokucommunity/roku-debug/compare/v0.11.0...v0.12.0) - 2022-05-17 +### Added + - `BSChannelPublishedEvent` custom event to allow clients to handle when the channel has been uploaded to a Roku ([#81](https://github.com/rokucommunity/roku-debug/pull/81)) + + + +## [0.11.0](https://github.com/rokucommunity/roku-debug/compare/v0.10.5...v0.11.0) - 2022-05-05 ### Added - `brightScriptConsolePort` option. Utilize `remotePort` in more places ([#79](https://github.com/rokucommunity/roku-debug/pull/79)) -basic breakpoint logic for debug protocol (only useful for direct API access at the moment) ([#77](https://github.com/rokucommunity/roku-debug/pull/77)) + ### Changed + - upgrade to [brighterscript@0.49.0](https://github.com/rokucommunity/brighterscript/blob/master/CHANGELOG.md#0490---2022-05-02) ### Fixed - fix RDB path bug on windows ([#76](https://github.com/rokucommunity/roku-debug/pull/76)) diff --git a/README.md b/README.md index d6996d01..246f1787 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,27 @@ # roku-debug A [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/) server (for editors like VSCode) and a socket adapter for Roku's [BrightScript Debug Protocol](https://developer.roku.com/en-ca/docs/developer-program/debugging/socket-based-debugger.md) -[![build status](https://img.shields.io/github/workflow/status/rokucommunity/roku-debug/build.svg?logo=github)](https://github.com/rokucommunity/roku-debug/actions?query=workflow%3Abuild) +[![build status](https://img.shields.io/github/actions/workflow/status/rokucommunity/roku-debug/build.yml?branch=master)](https://github.com/rokucommunity/roku-debug/actions?query=branch%3Amaster+workflow%3Abuild) [![coverage status](https://img.shields.io/coveralls/github/rokucommunity/roku-debug?logo=coveralls)](https://coveralls.io/github/rokucommunity/roku-debug?branch=master) [![monthly downloads](https://img.shields.io/npm/dm/roku-debug.svg?sanitize=true&logo=npm&logoColor=)](https://npmcharts.com/compare/roku-debug?minimal=true) [![npm version](https://img.shields.io/npm/v/roku-debug.svg?logo=npm)](https://www.npmjs.com/package/roku-debug) [![license](https://img.shields.io/github/license/rokucommunity/roku-debug.svg)](LICENSE) [![Slack](https://img.shields.io/badge/Slack-RokuCommunity-4A154B?logo=slack)](https://join.slack.com/t/rokudevelopers/shared_invite/zt-4vw7rg6v-NH46oY7hTktpRIBM_zGvwA) +## Usage +This project can be integrated with any IDE that supports the debug-adapter-protocol. + +**Known integrations:** +- [BrightScript Language extension for VSCode](https://github.com/rokucommunity/vscode-brightscript-language) +- [nvim-dap extension for Neovim](https://github.com/mfussenegger/nvim-dap/wiki/Debug-Adapter-installation#brightscript) + +## DAP instructions +To run the language server standalone, you simply need to: +- install nodejs and make sure npx is on your path +- install this project (`npm install roku-debug`) +- run the project in dap mode (`npx roku-debug --dap`) + + ## Contributors [![](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/images/0)](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/links/0)[![](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/images/1)](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/links/1)[![](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/images/2)](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/links/2)[![](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/images/3)](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/links/3)[![](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/images/4)](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/links/4)[![](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/images/5)](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/links/5)[![](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/images/6)](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/links/6)[![](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/images/7)](https://sourcerer.io/fame/TwitchBronBron/rokucommunity/roku-debug/links/7) diff --git a/package-lock.json b/package-lock.json index 9f9f7162..1dc4a4e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,52 +1,65 @@ { "name": "roku-debug", - "version": "0.11.0", + "version": "0.21.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-debug", - "version": "0.11.0", + "version": "0.21.9", "license": "MIT", "dependencies": { - "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.61.3", + "@rokucommunity/logger": "^0.3.9", + "@types/request": "^2.48.8", + "brighterscript": "^0.67.3", "clone-regexp": "2.2.0", "dateformat": "^4.6.3", + "debounce": "^1.2.1", "eol": "^0.9.1", "eventemitter3": "^4.0.7", "fast-glob": "^3.2.11", + "find-in-files": "^0.5.0", "fs-extra": "^10.0.0", "line-column": "^1.0.2", "natural-orderby": "^2.0.3", + "portfinder": "^1.0.32", + "postman-request": "^2.88.1-postman.32", "replace-in-file": "^6.3.2", "replace-last": "^1.2.6", - "roku-deploy": "^3.6.0", - "semver": "^7.3.5", + "roku-deploy": "^3.12.0", + "semver": "^7.5.4", "serialize-error": "^8.1.0", "smart-buffer": "^4.2.0", "source-map": "^0.7.4", "telnet-client": "^1.4.9", "vscode-debugadapter": "^1.49.0", "vscode-debugprotocol": "^1.49.0", - "vscode-languageserver": "^6.1.1" + "vscode-languageserver": "^6.1.1", + "xml2js": "^0.5.0" + }, + "bin": { + "roku-debug": "dist/cli.js" }, "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", "@types/fs-extra": "^9.0.13", "@types/line-column": "^1.0.0", "@types/mocha": "^9.0.0", "@types/node": "^16.11.6", - "@types/request": "^2.48.7", + "@types/request": "^2.48.8", "@types/semver": "^7.3.9", "@types/sinon": "^10.0.6", "@types/vscode": "^1.61.0", - "@typescript-eslint/eslint-plugin": "^5.2.0", - "@typescript-eslint/parser": "^5.2.0", + "@typescript-eslint/eslint-plugin": "^5.27.0", + "@typescript-eslint/parser": "^5.27.0", "chai": "^4.3.4", - "coveralls": "^3.1.1", + "coveralls-next": "^4.2.0", + "decompress": "^4.2.1", "dedent": "^0.7.0", "eslint": "^8.1.0", "eslint-plugin-no-only-tests": "^2.6.0", @@ -66,8 +79,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" }, @@ -77,16 +91,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", @@ -113,25 +129,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", @@ -143,16 +162,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", @@ -167,17 +188,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", @@ -189,8 +212,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" }, @@ -200,8 +224,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" }, @@ -211,8 +236,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" }, @@ -222,8 +248,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" }, @@ -233,8 +260,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", @@ -251,8 +279,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" }, @@ -262,8 +291,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", @@ -276,8 +306,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" }, @@ -287,8 +318,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" }, @@ -298,24 +330,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", @@ -327,8 +362,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", @@ -340,8 +376,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" }, @@ -351,8 +388,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", @@ -364,29 +402,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" }, @@ -396,8 +438,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" }, @@ -405,10 +448,22 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", + "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "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", @@ -420,8 +475,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", @@ -439,16 +495,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" @@ -459,16 +517,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" }, @@ -476,10 +536,47 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, "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", @@ -497,8 +594,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", @@ -510,13 +608,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", @@ -530,23 +630,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" @@ -557,14 +660,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" @@ -573,41 +678,93 @@ "node": ">= 8" } }, + "node_modules/@postman/form-data": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz", + "integrity": "sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@postman/tough-cookie": { + "version": "4.1.2-postman.2", + "resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.2-postman.2.tgz", + "integrity": "sha512-nrBdX3jA5HzlxTrGI/I0g6pmUKic7xbGA4fAMLFgmJCA3DL2Ma+3MvmD+Sdiw9gLEzZJIF4fz33sT8raV/L/PQ==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@postman/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@postman/tunnel-agent": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.3.tgz", + "integrity": "sha512-k57fzmAZ2PJGxfOA4SGR05ejorHbVAa/84Hxh/2nAztjNXc4ZjOm9NUIk6/Z6LCrBvJZqjRZbN8e/nROVUPVdg==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "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.0", - "resolved": "https://registry.npmjs.org/@rokucommunity/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-ycw6HaZG1td/LA4r380GaUggfX/TKBZNNPacUSrJDAlFmZpttDuIPA/txd1gi3zafBflHW36Rmv498HToqH3yg==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@rokucommunity/logger/-/logger-0.3.9.tgz", + "integrity": "sha512-j4DK7dF2klMhoclJZ6P7h1bT4bMVt/ynfCi3GSMozXld0M1HbpRyY3TCxu6dnxPl16l6PtIosNHELjQxKzFp6g==", "dependencies": { "chalk": "^4.1.2", + "date-fns": "^2.30.0", "fs-extra": "^10.0.0", + "parse-ms": "^2.1.0", "safe-json-stringify": "^1.2.0", "serialize-error": "^8.1.0" } }, "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", @@ -616,72 +773,105 @@ }, "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", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", + "dev": true }, "node_modules/@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 + }, + "node_modules/@types/dateformat": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/dateformat/-/dateformat-3.0.1.tgz", + "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", + "integrity": "sha512-/C8kTMRTNiNuWGl5nEyKbPiMv6HA+0RbEXzFhFBEzASM6+oa4tJro9b8nj7eRlOFfuLdzUU+DS/GPDlvvzMOhA==", "dev": true, - "license": "MIT" + "dependencies": { + "@types/node": "*" + } }, "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/json-schema": { - "version": "7.0.9", - "dev": true, - "license": "MIT" + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true }, "node_modules/@types/line-column": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/line-column/-/line-column-1.0.0.tgz", - "integrity": "sha512-wbw+IDRw/xY/RGy+BL6f4Eey4jsUgHQrMuA4Qj0CSG3x/7C2Oc57pmRoM2z3M4DkylWRz+G1pfX06sCXQm0J+w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/line-column/-/line-column-1.0.2.tgz", + "integrity": "sha512-099oFQmp/Tlf20xW5XI5R4F69N6lF/zQ09XDzc3R5BOLFlqIotgKoNIyj0HD4fQLWcGDreDJv8k/BkLJscrDrw==", "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", @@ -689,9 +879,10 @@ "license": "MIT" }, "node_modules/@types/request": { - "version": "2.48.7", + "version": "2.48.8", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", + "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/caseless": "*", "@types/node": "*", @@ -699,54 +890,48 @@ "form-data": "^2.5.0" } }, - "node_modules/@types/request/node_modules/form-data": { - "version": "2.5.1", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, "node_modules/@types/semver": { - "version": "7.3.9", - "dev": true, - "license": "MIT" + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "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" } }, "node_modules/@types/tough-cookie": { - "version": "4.0.1", - "dev": true, - "license": "MIT" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", + "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", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/experimental-utils": "5.2.0", - "@typescript-eslint/scope-manager": "5.2.0", - "debug": "^4.3.2", - "functional-red-black-tree": "^1.0.1", - "ignore": "^5.1.8", - "regexpp": "^3.2.0", - "semver": "^7.3.5", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", "tsutils": "^3.21.0" }, "engines": { @@ -766,25 +951,42 @@ } } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "5.1.8", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } }, - "node_modules/@typescript-eslint/experimental-utils": { - "version": "5.2.0", + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, - "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.2.0", - "@typescript-eslint/types": "5.2.0", - "@typescript-eslint/typescript-estree": "5.2.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -794,18 +996,58 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@typescript-eslint/parser": { - "version": "5.2.0", + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "5.2.0", - "@typescript-eslint/types": "5.2.0", - "@typescript-eslint/typescript-estree": "5.2.0", - "debug": "^4.3.2" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -815,7 +1057,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "*" }, "peerDependenciesMeta": { "typescript": { @@ -823,26 +1065,28 @@ } } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "5.2.0", + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.2.0", - "@typescript-eslint/visitor-keys": "5.2.0" + "ms": "2.1.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/@typescript-eslint/types": { - "version": "5.2.0", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", "dev": true, - "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -852,16 +1096,17 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.2.0", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "5.2.0", - "@typescript-eslint/visitor-keys": "5.2.0", - "debug": "^4.3.2", - "globby": "^11.0.4", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.5", + "semver": "^7.3.7", "tsutils": "^3.21.0" }, "engines": { @@ -877,13 +1122,57 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.2.0", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.2.0", - "eslint-visitor-keys": "^3.0.0" + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -894,36 +1183,44 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "3.0.0", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/@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" }, @@ -933,24 +1230,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" @@ -961,7 +1261,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", @@ -975,22 +1276,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" }, @@ -1003,7 +1307,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" @@ -1014,8 +1319,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" }, @@ -1025,65 +1331,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" @@ -1091,52 +1407,98 @@ }, "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==", + "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": "*" } }, "node_modules/aws4": { - "version": "1.11.0", - "license": "MIT" + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, "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", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "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" } }, + "node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "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" @@ -1144,7 +1506,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" }, @@ -1153,11 +1516,12 @@ } }, "node_modules/brighterscript": { - "version": "0.61.3", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.61.3.tgz", - "integrity": "sha512-8BDpOSCdmkS/QcTdPTUW/99nCBypuoa/Zz6PZHI6OiVylqTBidtrGI7lBZotqY6yvQ3KJl24thhLHK5XuIT/6w==", + "version": "0.67.3", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.67.3.tgz", + "integrity": "sha512-uuAIvDmIENA+HdeuRiC1KM+n250m0J9k0Cdiwpby4cBZiFAHRkYoynEqmL8kwAaPtxdXI5RcZU4JqInCd1avlA==", "dependencies": { "@rokucommunity/bslib": "^0.1.1", + "@rokucommunity/logger": "^0.3.9", "@xml-tools/parser": "^1.0.7", "array-flat-polyfill": "^1.0.1", "chalk": "^2.4.2", @@ -1167,9 +1531,9 @@ "cross-platform-clear-console": "^2.3.0", "debounce-promise": "^3.1.0", "eventemitter3": "^4.0.0", - "fast-glob": "^3.2.11", + "fast-glob": "^3.2.12", "file-url": "^3.0.0", - "fs-extra": "^8.0.0", + "fs-extra": "^8.1.0", "jsonc-parser": "^2.3.0", "long": "^3.2.0", "luxon": "^2.5.2", @@ -1177,15 +1541,17 @@ "moment": "^2.23.0", "p-settle": "^2.1.0", "parse-ms": "^2.1.0", + "readline": "^1.3.0", "require-relative": "^0.8.7", - "roku-deploy": "^3.9.3", + "roku-deploy": "^3.12.0", "serialize-error": "^7.0.1", "source-map": "^0.7.4", - "vscode-languageserver": "7.0.0", - "vscode-languageserver-protocol": "3.16.0", - "vscode-languageserver-textdocument": "^1.0.1", - "vscode-uri": "^2.1.1", - "xml2js": "^0.4.19", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.0.8", + "xml2js": "^0.5.0", "yargs": "^16.2.0" }, "bin": { @@ -1194,7 +1560,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" }, @@ -1204,7 +1571,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", @@ -1216,7 +1584,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", @@ -1225,14 +1594,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", @@ -1249,14 +1620,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" }, @@ -1269,7 +1642,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" }, @@ -1279,7 +1653,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" }, @@ -1288,10 +1663,11 @@ } }, "node_modules/brighterscript/node_modules/vscode-languageserver": { - "version": "7.0.0", - "license": "MIT", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", "dependencies": { - "vscode-languageserver-protocol": "3.16.0" + "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" @@ -1299,7 +1675,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", @@ -1314,7 +1691,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" }, @@ -1327,7 +1705,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" }, @@ -1337,18 +1716,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", @@ -1364,20 +1746,31 @@ }, "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" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "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", @@ -1396,15 +1789,72 @@ "url": "https://opencollective.com/browserslist" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true + }, "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", @@ -1417,24 +1867,26 @@ }, "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" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001332", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz", - "integrity": "sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw==", + "version": "1.0.30001620", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz", + "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==", "dev": true, "funding": [ { @@ -1444,17 +1896,23 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, "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", @@ -1469,7 +1927,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" @@ -1483,15 +1942,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" } @@ -1524,22 +1985,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", @@ -1559,7 +2024,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" }, @@ -1569,11 +2035,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" }, @@ -1581,58 +2049,104 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "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, + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "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", + "resolved": "https://registry.npmjs.org/coveralls-next/-/coveralls-next-4.2.0.tgz", + "integrity": "sha512-zg41a/4QDSASPtlV6gp+6owoU43U5CguxuPZR3nPZ26M5ZYdEK3MdUe7HwE+AnCZPkucudfhqqJZehCNkz2rYg==", + "dev": true, + "dependencies": { + "form-data": "4.0.0", + "js-yaml": "4.1.0", + "lcov-parse": "1.0.0", + "log-driver": "1.2.7", + "minimist": "1.2.7" + }, + "bin": { + "coveralls": "bin/coveralls.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/coveralls-next/node_modules/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 }, - "node_modules/convert-source-map": { - "version": "1.8.0", + "node_modules/coveralls-next/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dev": true, - "license": "MIT", "dependencies": { - "safe-buffer": "~5.1.1" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/coveralls": { - "version": "3.1.1", + "node_modules/coveralls-next/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": "BSD-2-Clause", "dependencies": { - "js-yaml": "^3.13.1", - "lcov-parse": "^1.0.0", - "log-driver": "^1.2.7", - "minimist": "^1.2.5", - "request": "^2.88.2" + "argparse": "^2.0.1" }, "bin": { - "coveralls": "bin/coveralls.js" - }, - "engines": { - "node": ">=6" + "js-yaml": "bin/js-yaml.js" } }, "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", @@ -1644,7 +2158,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" }, @@ -1652,21 +2167,43 @@ "node": ">=0.10" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/dateformat": { "version": "4.6.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", "engines": { "node": "*" } }, "node_modules/dayjs": { - "version": "1.11.7", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", - "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + "version": "1.11.2", + "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", @@ -1687,21 +2224,168 @@ }, "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "dependencies": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "dependencies": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "dependencies": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "dependencies": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "dev": true, + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip/node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/decompress/node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "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" }, @@ -1711,13 +2395,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" }, @@ -1727,31 +2413,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" }, @@ -1761,8 +2451,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" }, @@ -1772,7 +2463,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" @@ -1780,17 +2472,29 @@ }, "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", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } }, "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" }, @@ -1800,31 +2504,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", @@ -1877,16 +2586,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" @@ -1897,8 +2608,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" }, @@ -1914,21 +2626,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" }, @@ -1938,8 +2653,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" @@ -1950,24 +2666,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" }, @@ -1977,8 +2696,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" }, @@ -1988,8 +2708,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", @@ -2001,16 +2722,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" @@ -2021,8 +2744,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" }, @@ -2032,16 +2756,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" }, @@ -2051,51 +2777,58 @@ }, "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.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -2109,24 +2842,37 @@ }, "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" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, "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" }, @@ -2134,16 +2880,27 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "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" }, @@ -2151,10 +2908,19 @@ "node": ">=8" } }, + "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", @@ -2169,8 +2935,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" }, @@ -2178,10 +2945,20 @@ "node": ">=8" } }, + "node_modules/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==", + "dependencies": { + "find": "^0.1.5", + "q": "^1.0.1" + } + }, "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" @@ -2192,16 +2969,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" @@ -2217,8 +2996,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" @@ -2229,14 +3009,17 @@ }, "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": "*" } }, "node_modules/form-data": { - "version": "2.3.3", - "license": "MIT", + "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", @@ -2248,6 +3031,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": [ { @@ -2262,12 +3047,18 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true }, "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", @@ -2279,7 +3070,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" }, @@ -2289,18 +3081,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" @@ -2311,44 +3107,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" }, @@ -2356,16 +3158,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dev": true, + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "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", @@ -2383,12 +3200,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" }, @@ -2398,8 +3217,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" }, @@ -2411,15 +3231,16 @@ } }, "node_modules/globby": { - "version": "11.0.4", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, - "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", "slash": "^3.0.0" }, "engines": { @@ -2430,35 +3251,47 @@ } }, "node_modules/globby/node_modules/ignore": { - "version": "5.1.8", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "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/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, "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" @@ -2469,15 +3302,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" @@ -2491,16 +3326,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" } @@ -2510,35 +3347,45 @@ "dev": true, "license": "MIT" }, - "node_modules/http-signature": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "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" @@ -2552,31 +3399,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" @@ -2584,19 +3435,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" }, @@ -2606,21 +3460,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" }, @@ -2628,17 +3485,25 @@ "node": ">=0.10.0" } }, + "node_modules/is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "dev": true + }, "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" } @@ -2653,8 +3518,9 @@ }, "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" }, @@ -2664,12 +3530,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" }, @@ -2679,20 +3547,23 @@ }, "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/isobject": { "version": "2.1.0", @@ -2707,20 +3578,23 @@ }, "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" }, @@ -2730,8 +3604,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", @@ -2743,17 +3618,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", @@ -2769,8 +3646,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", @@ -2782,8 +3660,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", @@ -2795,16 +3674,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" @@ -2815,13 +3696,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" @@ -2832,12 +3715,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" }, @@ -2847,20 +3732,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", @@ -2876,47 +3765,39 @@ }, "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" } }, - "node_modules/jsprim": { - "version": "1.4.2", - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/jszip": { - "version": "3.7.1", - "license": "(MIT OR GPL-3.0-or-later)", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", - "set-immediate-shim": "~1.0.1" + "setimmediate": "^1.0.5" } }, "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" } @@ -2931,8 +3812,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" @@ -2943,7 +3825,8 @@ }, "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" } @@ -2959,8 +3842,9 @@ }, "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" }, @@ -2968,20 +3852,28 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "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", @@ -2993,8 +3885,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" @@ -3008,14 +3901,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" }, @@ -3033,8 +3928,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" }, @@ -3046,28 +3942,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" @@ -3077,17 +3977,19 @@ } }, "node_modules/mime-db": { - "version": "1.50.0", - "license": "MIT", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.33", - "license": "MIT", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dependencies": { - "mime-db": "1.50.0" + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" @@ -3105,14 +4007,17 @@ } }, "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "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" }, @@ -3165,13 +4070,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", @@ -3180,8 +4087,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" }, @@ -3191,8 +4099,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" @@ -3206,8 +4115,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" }, @@ -3217,8 +4127,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" }, @@ -3243,13 +4154,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" }, @@ -3262,8 +4175,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" }, @@ -3276,8 +4190,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" }, @@ -3290,8 +4205,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", @@ -3306,16 +4222,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", @@ -3331,8 +4249,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" } @@ -3347,8 +4266,8 @@ }, "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==" }, "node_modules/nanoid": { "version": "3.3.1", @@ -3364,20 +4283,29 @@ }, "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-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "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", @@ -3388,8 +4316,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" }, @@ -3399,20 +4328,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", @@ -3451,30 +4383,43 @@ }, "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": "*" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "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", @@ -3489,8 +4434,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" }, @@ -3500,8 +4446,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" }, @@ -3514,8 +4461,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" }, @@ -3525,8 +4473,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" }, @@ -3536,14 +4485,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" @@ -3554,7 +4505,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" }, @@ -3564,23 +4516,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", @@ -3593,12 +4548,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" }, @@ -3608,98 +4565,247 @@ }, "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": "*" + "node": "*" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "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", + "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", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "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==", + "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==", + "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==", + "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", + "integrity": "sha512-Zf5D0b2G/UmnmjRwQKhYy4TBkuahwD0AMNyWwFK3atxU1u5GS38gdd7aw3vyR6E7Ii+gD//hREpflj2dmpbE7w==", + "dependencies": { + "@postman/form-data": "~3.1.1", + "@postman/tough-cookie": "~4.1.2-postman.1", + "@postman/tunnel-agent": "^0.6.3", + "aws-sign2": "~0.7.0", + "aws4": "^1.12.0", + "brotli": "^1.3.3", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "har-validator": "~5.1.3", + "http-signature": "~1.3.1", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "^2.1.35", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.3", + "safe-buffer": "^5.1.2", + "stream-length": "^1.0.2", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postman-request/node_modules/http-signature": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" + }, + "engines": { + "node": ">=0.10" } }, - "node_modules/performance-now": { - "version": "2.1.0", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.0.0", - "dev": true, - "license": "ISC" + "node_modules/postman-request/node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } }, - "node_modules/picomatch": { - "version": "2.3.0", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node_modules/postman-request/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" } }, "node_modules/prelude-ls": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/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" }, @@ -3709,23 +4815,35 @@ }, "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", + "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" + } + }, "node_modules/qs": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", @@ -3734,8 +4852,15 @@ "node": ">=0.6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "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", @@ -3749,20 +4874,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", @@ -3784,14 +4910,26 @@ "node": ">=8.10.0" } }, + "node_modules/readline": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", + "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==" + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "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" }, @@ -3801,8 +4939,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" }, @@ -3812,7 +4951,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", @@ -3827,7 +4967,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", @@ -3836,7 +4977,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", @@ -3851,14 +4993,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", @@ -3874,75 +5018,57 @@ }, "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/request": { - "version": "2.88.2", - "license": "Apache-2.0", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, "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", "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==" }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "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" @@ -3950,8 +5076,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" }, @@ -3964,8 +5091,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", @@ -3976,8 +5104,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" }, @@ -3986,24 +5115,25 @@ } }, "node_modules/roku-deploy": { - "version": "3.9.3", - "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.9.3.tgz", - "integrity": "sha512-cjTx5ffZNt07rQS+0s2sTBHkZKUk283y9f6UnbI77X03lQ60vYlCnqsKswWisFYMHPIdvsTLLSfKsshAPwKHEQ==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.12.0.tgz", + "integrity": "sha512-YiCZeQ+sEmFW9ZfXtMNH+/CBSHQ5deNZYWONM+s6gCEQsrz7kCMFPj5YEdgfqW+d2b8G1ve9GELHcSt2FsfM8g==", "dependencies": { "chalk": "^2.4.2", "dateformat": "^3.0.3", "dayjs": "^1.11.0", - "fast-glob": "^3.2.11", + "fast-glob": "^3.2.12", "fs-extra": "^7.0.1", "is-glob": "^4.0.3", "jsonc-parser": "^2.3.0", "jszip": "^3.6.0", + "lodash": "^4.17.21", + "micromatch": "^4.0.4", "moment": "^2.29.1", "parse-ms": "^2.1.0", - "picomatch": "^2.2.1", - "request": "^2.88.0", + "postman-request": "^2.88.1-postman.32", "temp-dir": "^2.0.0", - "xml2js": "^0.4.23" + "xml2js": "^0.5.0" }, "bin": { "roku-deploy": "dist/cli.js" @@ -4011,7 +5141,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" }, @@ -4021,7 +5152,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", @@ -4033,25 +5165,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", @@ -4063,14 +5199,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" }, @@ -4080,6 +5218,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", @@ -4094,43 +5234,62 @@ "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", - "license": "ISC" + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "node_modules/seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dev": true, + "dependencies": { + "commander": "^2.8.1" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } }, "node_modules/semver": { - "version": "7.3.5", - "license": "ISC", + "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" }, @@ -4143,7 +5302,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" }, @@ -4156,28 +5316,29 @@ }, "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/set-immediate-shim": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, "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" }, @@ -4187,21 +5348,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", @@ -4217,15 +5381,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" @@ -4241,8 +5407,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" @@ -4250,16 +5417,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", @@ -4274,12 +5443,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", @@ -4291,20 +5462,40 @@ "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" } }, + "node_modules/stream-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz", + "integrity": "sha512-aI+qKFiwoDV4rsXiS7WRoCt+v2RX1nUj17+KJC5r2gfh5xoSJIfP6Y3Do/HtvesFcTSWthIuJ3l1cvKQY/+nZg==", + "dependencies": { + "bluebird": "^2.6.2" + } + }, + "node_modules/stream-length/node_modules/bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==" + }, "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", @@ -4316,7 +5507,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" }, @@ -4324,10 +5516,20 @@ "node": ">=8" } }, + "node_modules/strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "dependencies": { + "is-natural-number": "^4.0.1" + } + }, "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" }, @@ -4337,7 +5539,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" }, @@ -4345,9 +5548,28 @@ "node": ">=8" } }, + "node_modules/tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "dependencies": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "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" }, @@ -4366,8 +5588,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", @@ -4379,20 +5602,35 @@ }, "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", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "dev": true }, "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" }, @@ -4400,21 +5638,16 @@ "node": ">=8.0" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } + "node_modules/traverse-chain": { + "version": "0.1.0", + "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", @@ -4453,29 +5686,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" }, @@ -4486,24 +5723,16 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "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" }, @@ -4513,15 +5742,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" }, @@ -4531,16 +5762,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", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz", + "integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4549,6 +5782,16 @@ "node": ">=4.2.0" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "node_modules/undent": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/undent/-/undent-0.1.0.tgz", @@ -4557,24 +5800,37 @@ }, "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" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "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", + "dev": true, "license": "MIT", "bin": { "uuid": "bin/uuid" @@ -4582,13 +5838,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", @@ -4598,18 +5856,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", @@ -4618,11 +5878,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" @@ -4630,18 +5893,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", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", "engines": { - "node": ">=8.0.0 || >=10.0.0" + "node": ">=14.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" }, @@ -4650,29 +5917,34 @@ } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.16.0", - "license": "MIT", + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "dependencies": { - "vscode-jsonrpc": "6.0.0", - "vscode-languageserver-types": "3.16.0" + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" } }, "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.2", - "license": "MIT" + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz", + "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==" }, "node_modules/vscode-languageserver-types": { - "version": "3.16.0", - "license": "MIT" + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, "node_modules/vscode-uri": { - "version": "2.1.2", - "license": "MIT" + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" }, "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" }, @@ -4685,13 +5957,15 @@ }, "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.3", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4704,8 +5978,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", @@ -4717,12 +5992,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", @@ -4731,8 +6008,9 @@ } }, "node_modules/xml2js": { - "version": "0.4.23", - "license": "MIT", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" @@ -4743,24 +6021,37 @@ }, "node_modules/xmlbuilder": { "version": "11.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "engines": { "node": ">=4.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, "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", @@ -4780,8 +6071,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" @@ -4792,8 +6084,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", @@ -4806,8 +6099,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" }, @@ -4817,8 +6111,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" }, @@ -4826,10 +6121,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "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" }, @@ -4841,6 +6147,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" @@ -4848,10 +6156,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", @@ -4872,17 +6184,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", @@ -4892,12 +6210,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", @@ -4907,13 +6229,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", @@ -4923,6 +6249,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" @@ -4930,6 +6258,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" @@ -4937,6 +6267,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" @@ -4944,6 +6276,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" @@ -4951,6 +6285,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", @@ -4965,6 +6301,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" @@ -4972,6 +6310,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", @@ -4982,6 +6322,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" @@ -4989,6 +6331,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" @@ -4996,14 +6340,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", @@ -5013,6 +6363,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", @@ -5022,6 +6374,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" @@ -5029,6 +6383,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", @@ -5038,6 +6394,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" @@ -5045,14 +6403,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" @@ -5062,10 +6426,22 @@ }, "@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/runtime": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", + "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, "@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", @@ -5075,6 +6451,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", @@ -5090,12 +6468,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", @@ -5104,17 +6486,46 @@ }, "@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" } }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + } + } + }, + "@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true + }, "@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", @@ -5130,6 +6541,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", @@ -5139,10 +6552,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", @@ -5154,47 +6571,99 @@ "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" } }, + "@postman/form-data": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz", + "integrity": "sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "@postman/tough-cookie": { + "version": "4.1.2-postman.2", + "resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.2-postman.2.tgz", + "integrity": "sha512-nrBdX3jA5HzlxTrGI/I0g6pmUKic7xbGA4fAMLFgmJCA3DL2Ma+3MvmD+Sdiw9gLEzZJIF4fz33sT8raV/L/PQ==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "dependencies": { + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" + } + } + }, + "@postman/tunnel-agent": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.3.tgz", + "integrity": "sha512-k57fzmAZ2PJGxfOA4SGR05ejorHbVAa/84Hxh/2nAztjNXc4ZjOm9NUIk6/Z6LCrBvJZqjRZbN8e/nROVUPVdg==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "@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.0", - "resolved": "https://registry.npmjs.org/@rokucommunity/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-ycw6HaZG1td/LA4r380GaUggfX/TKBZNNPacUSrJDAlFmZpttDuIPA/txd1gi3zafBflHW36Rmv498HToqH3yg==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@rokucommunity/logger/-/logger-0.3.9.tgz", + "integrity": "sha512-j4DK7dF2klMhoclJZ6P7h1bT4bMVt/ynfCi3GSMozXld0M1HbpRyY3TCxu6dnxPl16l6PtIosNHELjQxKzFp6g==", "requires": { "chalk": "^4.1.2", + "date-fns": "^2.30.0", "fs-extra": "^10.0.0", + "parse-ms": "^2.1.0", "safe-json-stringify": "^1.2.0", "serialize-error": "^8.1.0" } }, "@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" @@ -5202,6 +6671,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" @@ -5209,6 +6680,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", @@ -5218,59 +6691,104 @@ }, "@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==", "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": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/dateformat/-/dateformat-3.0.1.tgz", + "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", + "integrity": "sha512-/C8kTMRTNiNuWGl5nEyKbPiMv6HA+0RbEXzFhFBEzASM6+oa4tJro9b8nj7eRlOFfuLdzUU+DS/GPDlvvzMOhA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@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": "*" } }, "@types/json-schema": { - "version": "7.0.9", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "@types/line-column": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/line-column/-/line-column-1.0.0.tgz", - "integrity": "sha512-wbw+IDRw/xY/RGy+BL6f4Eey4jsUgHQrMuA4Qj0CSG3x/7C2Oc57pmRoM2z3M4DkylWRz+G1pfX06sCXQm0J+w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/line-column/-/line-column-1.0.2.tgz", + "integrity": "sha512-099oFQmp/Tlf20xW5XI5R4F69N6lF/zQ09XDzc3R5BOLFlqIotgKoNIyj0HD4fQLWcGDreDJv8k/BkLJscrDrw==", "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": { @@ -5278,138 +6796,219 @@ "dev": true }, "@types/request": { - "version": "2.48.7", + "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": "*", "@types/tough-cookie": "*", "form-data": "^2.5.0" - }, - "dependencies": { - "form-data": { - "version": "2.5.1", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - } } }, "@types/semver": { - "version": "7.3.9", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "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" } }, "@types/tough-cookie": { - "version": "4.0.1", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", + "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", - "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "5.2.0", - "@typescript-eslint/scope-manager": "5.2.0", - "debug": "^4.3.2", - "functional-red-black-tree": "^1.0.1", - "ignore": "^5.1.8", - "regexpp": "^3.2.0", - "semver": "^7.3.5", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", "tsutils": "^3.21.0" }, "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, "ignore": { - "version": "5.1.8", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true } } }, - "@typescript-eslint/experimental-utils": { - "version": "5.2.0", + "@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "requires": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.2.0", - "@typescript-eslint/types": "5.2.0", - "@typescript-eslint/typescript-estree": "5.2.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } } }, - "@typescript-eslint/parser": { - "version": "5.2.0", + "@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.2.0", - "@typescript-eslint/types": "5.2.0", - "@typescript-eslint/typescript-estree": "5.2.0", - "debug": "^4.3.2" + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" } }, - "@typescript-eslint/scope-manager": { - "version": "5.2.0", + "@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", "dev": true, "requires": { - "@typescript-eslint/types": "5.2.0", - "@typescript-eslint/visitor-keys": "5.2.0" + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } } }, "@typescript-eslint/types": { - "version": "5.2.0", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.2.0", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.2.0", - "@typescript-eslint/visitor-keys": "5.2.0", - "debug": "^4.3.2", - "globby": "^11.0.4", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.5", + "semver": "^7.3.7", "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } + } + }, + "@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { - "version": "5.2.0", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.2.0", - "eslint-visitor-keys": "^3.0.0" + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" }, "dependencies": { "eslint-visitor-keys": { - "version": "3.0.0", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true } } }, "@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" } @@ -5418,19 +7017,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", @@ -5439,6 +7046,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", @@ -5448,19 +7057,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" @@ -5468,6 +7085,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" @@ -5475,45 +7094,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", @@ -5522,34 +7161,75 @@ }, "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==", + "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.11.0" + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "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", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "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", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } }, "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" @@ -5557,16 +7237,19 @@ }, "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" } }, "brighterscript": { - "version": "0.61.3", - "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.61.3.tgz", - "integrity": "sha512-8BDpOSCdmkS/QcTdPTUW/99nCBypuoa/Zz6PZHI6OiVylqTBidtrGI7lBZotqY6yvQ3KJl24thhLHK5XuIT/6w==", + "version": "0.67.3", + "resolved": "https://registry.npmjs.org/brighterscript/-/brighterscript-0.67.3.tgz", + "integrity": "sha512-uuAIvDmIENA+HdeuRiC1KM+n250m0J9k0Cdiwpby4cBZiFAHRkYoynEqmL8kwAaPtxdXI5RcZU4JqInCd1avlA==", "requires": { "@rokucommunity/bslib": "^0.1.1", + "@rokucommunity/logger": "^0.3.9", "@xml-tools/parser": "^1.0.7", "array-flat-polyfill": "^1.0.1", "chalk": "^2.4.2", @@ -5576,9 +7259,9 @@ "cross-platform-clear-console": "^2.3.0", "debounce-promise": "^3.1.0", "eventemitter3": "^4.0.0", - "fast-glob": "^3.2.11", + "fast-glob": "^3.2.12", "file-url": "^3.0.0", - "fs-extra": "^8.0.0", + "fs-extra": "^8.1.0", "jsonc-parser": "^2.3.0", "long": "^3.2.0", "luxon": "^2.5.2", @@ -5586,26 +7269,32 @@ "moment": "^2.23.0", "p-settle": "^2.1.0", "parse-ms": "^2.1.0", + "readline": "^1.3.0", "require-relative": "^0.8.7", - "roku-deploy": "^3.9.3", + "roku-deploy": "^3.12.0", "serialize-error": "^7.0.1", "source-map": "^0.7.4", - "vscode-languageserver": "7.0.0", - "vscode-languageserver-protocol": "3.16.0", - "vscode-languageserver-textdocument": "^1.0.1", - "vscode-uri": "^2.1.1", - "xml2js": "^0.4.19", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.0.8", + "xml2js": "^0.5.0", "yargs": "^16.2.0" }, "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", @@ -5614,6 +7303,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", @@ -5622,12 +7313,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", @@ -5640,31 +7335,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", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", "requires": { - "vscode-languageserver-protocol": "3.16.0" + "vscode-languageserver-protocol": "3.17.5" } }, "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", @@ -5673,26 +7380,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", @@ -5704,16 +7421,30 @@ } }, "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==" } } }, + "brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "requires": { + "base64-js": "^1.1.2" + } + }, "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", @@ -5723,12 +7454,54 @@ "picocolors": "^1.0.0" } }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true + }, "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", @@ -5739,23 +7512,31 @@ }, "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": { - "version": "1.0.30001332", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz", - "integrity": "sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw==", + "version": "1.0.30001620", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz", + "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==", "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", @@ -5768,6 +7549,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" @@ -5775,10 +7558,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" } @@ -5800,13 +7587,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", @@ -5824,56 +7617,112 @@ }, "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" } }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "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": { - "version": "3.1.1", + "coveralls-next": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/coveralls-next/-/coveralls-next-4.2.0.tgz", + "integrity": "sha512-zg41a/4QDSASPtlV6gp+6owoU43U5CguxuPZR3nPZ26M5ZYdEK3MdUe7HwE+AnCZPkucudfhqqJZehCNkz2rYg==", "dev": true, "requires": { - "js-yaml": "^3.13.1", - "lcov-parse": "^1.0.0", - "log-driver": "^1.2.7", - "minimist": "^1.2.5", - "request": "^2.88.2" + "form-data": "4.0.0", + "js-yaml": "4.1.0", + "lcov-parse": "1.0.0", + "log-driver": "1.2.7", + "minimist": "1.2.7" + }, + "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 + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "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" + } + } } }, "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", @@ -5883,20 +7732,39 @@ }, "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" } }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } + }, "dateformat": { - "version": "4.6.3" + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==" }, "dayjs": { - "version": "1.11.7", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", - "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + "version": "1.11.2", + "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", @@ -5909,14 +7777,140 @@ }, "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": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "requires": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + } + } + } + } + }, + "decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "requires": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "dependencies": { + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true + } + } + }, + "decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "dependencies": { + "file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true + } + } + }, + "decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "dependencies": { + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true + } + } + }, + "decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "dev": true, + "requires": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "dependencies": { + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true + } + } + }, "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" @@ -5924,10 +7918,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" @@ -5935,19 +7933,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" @@ -5955,6 +7961,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" @@ -5962,6 +7970,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" @@ -5969,33 +7979,58 @@ }, "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" + "emoji-regex": { + "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", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } }, "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", @@ -6040,14 +8075,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", @@ -6056,14 +8097,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" @@ -6071,6 +8118,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" @@ -6080,10 +8129,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", @@ -6092,6 +8145,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" @@ -6099,10 +8154,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", @@ -6112,16 +8171,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" @@ -6129,12 +8194,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" @@ -6142,34 +8211,48 @@ "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.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -6179,36 +8262,73 @@ } }, "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" } }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, "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" } }, + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "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", @@ -6218,6 +8338,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" @@ -6225,8 +8347,19 @@ } } }, + "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" + } + }, "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", @@ -6235,10 +8368,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", @@ -6251,6 +8388,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", @@ -6258,10 +8397,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.3.3", + "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", @@ -6270,10 +8414,20 @@ }, "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": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true }, "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", @@ -6282,54 +8436,88 @@ "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": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + } + }, "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", @@ -6341,61 +8529,89 @@ }, "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" } }, "globby": { - "version": "11.0.4", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "requires": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", "slash": "^3.0.0" }, "dependencies": { "ignore": { - "version": "5.1.8", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "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==" + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, "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", @@ -6404,35 +8620,43 @@ "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": { "version": "2.0.2", "dev": true }, - "http-signature": { - "version": "1.2.0", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true }, "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", @@ -6441,14 +8665,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" @@ -6456,16 +8686,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" @@ -6473,27 +8709,45 @@ }, "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" } }, + "is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "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-regexp": { @@ -6503,24 +8757,36 @@ }, "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 }, "isobject": { @@ -6532,14 +8798,20 @@ } }, "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" @@ -6547,6 +8819,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", @@ -6556,13 +8830,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", @@ -6576,6 +8854,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", @@ -6585,6 +8865,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", @@ -6594,12 +8876,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", @@ -6608,10 +8894,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", @@ -6619,24 +8909,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", @@ -6645,38 +8947,39 @@ "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" } }, - "jsprim": { - "version": "1.4.2", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, "jszip": { - "version": "3.7.1", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "requires": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", - "set-immediate-shim": "~1.0.1" + "setimmediate": "^1.0.5" } }, "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": { @@ -6685,6 +8988,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", @@ -6693,6 +8998,8 @@ }, "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" } @@ -6708,21 +9015,34 @@ }, "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==" + }, "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": { @@ -6731,6 +9051,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", @@ -6738,10 +9060,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" } @@ -6753,38 +9079,52 @@ }, "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" } }, "mime-db": { - "version": "1.50.0" + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { - "version": "2.1.33", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "requires": { - "mime-db": "1.50.0" + "mime-db": "1.52.0" } }, "minimatch": { @@ -6796,13 +9136,14 @@ } }, "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "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", @@ -6838,10 +9179,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", @@ -6851,10 +9196,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", @@ -6863,6 +9212,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" @@ -6870,6 +9221,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" @@ -6886,10 +9239,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" @@ -6897,6 +9254,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" @@ -6904,6 +9263,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" @@ -6911,6 +9272,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", @@ -6920,10 +9283,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", @@ -6937,6 +9304,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 } } @@ -6948,7 +9317,8 @@ }, "ms": { "version": "2.1.2", - "dev": true + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nanoid": { "version": "3.3.1", @@ -6958,13 +9328,25 @@ }, "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-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "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", @@ -6976,6 +9358,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" @@ -6983,13 +9367,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", @@ -7023,21 +9413,35 @@ "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", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true }, "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", @@ -7050,10 +9454,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" @@ -7061,6 +9469,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" @@ -7068,16 +9478,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" @@ -7085,21 +9501,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", @@ -7109,31 +9533,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" @@ -7141,37 +9579,167 @@ "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": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "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", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "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==", + "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==", + "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", + "integrity": "sha512-Zf5D0b2G/UmnmjRwQKhYy4TBkuahwD0AMNyWwFK3atxU1u5GS38gdd7aw3vyR6E7Ii+gD//hREpflj2dmpbE7w==", + "requires": { + "@postman/form-data": "~3.1.1", + "@postman/tough-cookie": "~4.1.2-postman.1", + "@postman/tunnel-agent": "^0.6.3", + "aws-sign2": "~0.7.0", + "aws4": "^1.12.0", + "brotli": "^1.3.3", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "har-validator": "~5.1.3", + "http-signature": "~1.3.1", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "^2.1.35", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.3", + "safe-buffer": "^5.1.2", + "stream-length": "^1.0.2", + "uuid": "^8.3.2" + }, + "dependencies": { + "http-signature": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" + } + }, + "jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } }, "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" @@ -7179,24 +9747,44 @@ }, "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", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==" }, "qs": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "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" @@ -7204,6 +9792,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", @@ -7222,15 +9812,31 @@ "picomatch": "^2.2.1" } }, + "readline": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", + "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==" + }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "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" @@ -7238,6 +9844,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", @@ -7246,6 +9854,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", @@ -7254,6 +9864,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", @@ -7261,10 +9873,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", @@ -7276,43 +9892,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" - }, - "request": { - "version": "2.88.2", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } + "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": { @@ -7320,15 +9919,26 @@ "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==" }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "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" @@ -7336,6 +9946,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", @@ -7347,6 +9959,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" @@ -7355,34 +9969,39 @@ } }, "roku-deploy": { - "version": "3.9.3", - "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.9.3.tgz", - "integrity": "sha512-cjTx5ffZNt07rQS+0s2sTBHkZKUk283y9f6UnbI77X03lQ60vYlCnqsKswWisFYMHPIdvsTLLSfKsshAPwKHEQ==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/roku-deploy/-/roku-deploy-3.12.0.tgz", + "integrity": "sha512-YiCZeQ+sEmFW9ZfXtMNH+/CBSHQ5deNZYWONM+s6gCEQsrz7kCMFPj5YEdgfqW+d2b8G1ve9GELHcSt2FsfM8g==", "requires": { "chalk": "^2.4.2", "dateformat": "^3.0.3", "dayjs": "^1.11.0", - "fast-glob": "^3.2.11", + "fast-glob": "^3.2.12", "fs-extra": "^7.0.1", "is-glob": "^4.0.3", "jsonc-parser": "^2.3.0", "jszip": "^3.6.0", + "lodash": "^4.17.21", + "micromatch": "^4.0.4", "moment": "^2.29.1", "parse-ms": "^2.1.0", - "picomatch": "^2.2.1", - "request": "^2.88.0", + "postman-request": "^2.88.1-postman.32", "temp-dir": "^2.0.0", - "xml2js": "^0.4.23" + "xml2js": "^0.5.0" }, "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", @@ -7391,18 +10010,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", @@ -7410,10 +10037,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" } @@ -7422,12 +10053,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" @@ -7435,36 +10070,61 @@ "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" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dev": true, + "requires": { + "commander": "^2.8.1" + } }, "semver": { - "version": "7.3.5", + "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" @@ -7472,13 +10132,19 @@ }, "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 }, - "set-immediate-shim": { - "version": "1.0.1" + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, "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" @@ -7486,14 +10152,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", @@ -7506,10 +10178,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", @@ -7518,6 +10194,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", @@ -7526,12 +10204,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", @@ -7544,10 +10226,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", @@ -7560,14 +10246,33 @@ "tweetnacl": "~0.14.0" } }, + "stream-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz", + "integrity": "sha512-aI+qKFiwoDV4rsXiS7WRoCt+v2RX1nUj17+KJC5r2gfh5xoSJIfP6Y3Do/HtvesFcTSWthIuJ3l1cvKQY/+nZg==", + "requires": { + "bluebird": "^2.6.2" + }, + "dependencies": { + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==" + } + } + }, "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", @@ -7576,22 +10281,54 @@ }, "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" } }, + "strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "requires": { + "is-natural-number": "^4.0.1" + } + }, "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" } }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + }, "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" } @@ -7603,6 +10340,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", @@ -7612,27 +10351,45 @@ }, "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": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", "dev": true }, "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" } }, - "tough-cookie": { - "version": "2.5.0", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } + "traverse-chain": { + "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", @@ -7651,36 +10408,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" } }, - "tunnel-agent": { - "version": "0.6.0", - "requires": { - "safe-buffer": "^5.0.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" @@ -7688,22 +10451,40 @@ }, "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" } }, "typescript": { - "version": "4.4.4", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz", + "integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==", "dev": true }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "undent": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/undent/-/undent-0.1.0.tgz", @@ -7711,26 +10492,46 @@ "dev": true }, "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" } }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "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" + "version": "3.4.0", + "dev": true }, "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", @@ -7741,12 +10542,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", @@ -7754,47 +10559,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": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==" }, "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", + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "requires": { - "vscode-jsonrpc": "6.0.0", - "vscode-languageserver-types": "3.16.0" + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" } }, "vscode-languageserver-textdocument": { - "version": "1.0.2" + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz", + "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==" }, "vscode-languageserver-types": { - "version": "3.16.0" + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, "vscode-uri": { - "version": "2.1.2" + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" }, "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" @@ -7802,10 +10627,14 @@ }, "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": { - "version": "1.2.3", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true }, "workerpool": { @@ -7816,6 +10645,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", @@ -7824,10 +10655,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", @@ -7837,24 +10672,40 @@ } }, "xml2js": { - "version": "0.4.23", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "requires": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "xmlbuilder": { - "version": "11.0.1" + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true }, "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", @@ -7872,6 +10723,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", @@ -7880,6 +10733,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", @@ -7890,16 +10745,32 @@ "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 } } }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "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 166f47ca..8bb21579 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-debug", - "version": "0.11.0", + "version": "0.21.9", "description": "Debug adapter for Roku application development using Node.js", "main": "dist/index.js", "scripts": { @@ -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", @@ -17,6 +18,9 @@ "type": "git", "url": "https://github.com/rokucommunity/roku-debug" }, + "bin": { + "roku-debug": "dist/cli.js" + }, "author": "RokuCommunity", "license": "MIT", "mocha": { @@ -24,6 +28,10 @@ "source-map-support/register", "ts-node/register" ], + "watchFiles": [ + "src/**/*" + ], + "timeout": 2000, "fullTrace": true, "watchExtensions": [ "ts" @@ -54,20 +62,24 @@ }, "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", "@types/fs-extra": "^9.0.13", "@types/line-column": "^1.0.0", "@types/mocha": "^9.0.0", "@types/node": "^16.11.6", - "@types/request": "^2.48.7", + "@types/request": "^2.48.8", "@types/semver": "^7.3.9", "@types/sinon": "^10.0.6", "@types/vscode": "^1.61.0", - "@typescript-eslint/eslint-plugin": "^5.2.0", - "@typescript-eslint/parser": "^5.2.0", + "@typescript-eslint/eslint-plugin": "^5.27.0", + "@typescript-eslint/parser": "^5.27.0", "chai": "^4.3.4", - "coveralls": "^3.1.1", + "coveralls-next": "^4.2.0", + "decompress": "^4.2.1", "dedent": "^0.7.0", "eslint": "^8.1.0", "eslint-plugin-no-only-tests": "^2.6.0", @@ -85,26 +97,32 @@ "undent": "^0.1.0" }, "dependencies": { - "@rokucommunity/logger": "^0.3.0", - "brighterscript": "^0.61.3", + "@rokucommunity/logger": "^0.3.9", + "@types/request": "^2.48.8", + "brighterscript": "^0.67.3", "clone-regexp": "2.2.0", "dateformat": "^4.6.3", + "debounce": "^1.2.1", "eol": "^0.9.1", "eventemitter3": "^4.0.7", "fast-glob": "^3.2.11", + "find-in-files": "^0.5.0", "fs-extra": "^10.0.0", "line-column": "^1.0.2", "natural-orderby": "^2.0.3", + "portfinder": "^1.0.32", + "postman-request": "^2.88.1-postman.32", "replace-in-file": "^6.3.2", "replace-last": "^1.2.6", - "roku-deploy": "^3.6.0", - "semver": "^7.3.5", + "roku-deploy": "^3.12.0", + "semver": "^7.5.4", "serialize-error": "^8.1.0", "smart-buffer": "^4.2.0", "source-map": "^0.7.4", "telnet-client": "^1.4.9", "vscode-debugadapter": "^1.49.0", "vscode-debugprotocol": "^1.49.0", - "vscode-languageserver": "^6.1.1" + "vscode-languageserver": "^6.1.1", + "xml2js": "^0.5.0" } } diff --git a/src/CompileErrorProcessor.spec.ts b/src/CompileErrorProcessor.spec.ts index ba6904ff..c8b8e5c7 100644 --- a/src/CompileErrorProcessor.spec.ts +++ b/src/CompileErrorProcessor.spec.ts @@ -1,10 +1,13 @@ -import type { BrightScriptDebugCompileError } from './CompileErrorProcessor'; +import type { BSDebugDiagnostic } from './CompileErrorProcessor'; import { CompileErrorProcessor, CompileStatus } from './CompileErrorProcessor'; -import { expect, assert } from 'chai'; +import { expect } from 'chai'; +import type { SinonFakeTimers } from 'sinon'; import { createSandbox } from 'sinon'; +import { DiagnosticSeverity, util as bscUtil } from 'brighterscript'; +import dedent = require('dedent'); const sinon = createSandbox(); -describe('BrightScriptDebugger', () => { +describe('CompileErrorProcessor', () => { let compiler: CompileErrorProcessor; beforeEach(() => { @@ -15,109 +18,341 @@ describe('BrightScriptDebugger', () => { }); afterEach(() => { - compiler = undefined; sinon.restore(); + compiler.destroy(); + compiler = undefined; }); - describe('getSingleFileXmlError ', () => { - it('tests no input', () => { - let input = ['']; - let errors = compiler.getSingleFileXmlError(input); - assert.isEmpty(errors); + describe('events', () => { + let clock: SinonFakeTimers; + beforeEach(() => { + clock = sinon.useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('it allows unsubscribing', () => { + let count = 0; + const unobserve = compiler.on('diagnostics', () => { + count++; + unobserve(); + }); + compiler['emit']('diagnostics'); + compiler['emit']('diagnostics'); + + clock.tick(200); + expect(count).to.eql(1); }); - it('tests no match', () => { - let input = ['some other output']; - let errors = compiler.getSingleFileXmlError(input); - assert.isEmpty(errors); + it('does not throw when emitter is destroyed', () => { + const unobserve = compiler.on('diagnostics', () => { }); + delete compiler['emitter']; + unobserve(); + compiler['emit']('diagnostics'); + clock.tick(200); + //test passes because no exception was thrown }); - it('tests no match multiline', () => { - let input = [`multiline text`, `with no match`]; - let errors = compiler.getSingleFileXmlError(input); - assert.isEmpty(errors); + it('skips emitting the event when there are zero errros', () => { + let callCount = 0; + const unobserve = compiler.on('diagnostics', () => { + callCount++; + }); + compiler['reportErrors'](); + clock.tick(200); + expect(callCount).to.equal(0); }); - it('match', () => { - let input = [`-------> Error parsing XML component SimpleEntitlements.xml`]; - let errors = compiler.getSingleFileXmlError(input); - assert.lengthOf(errors, 1); - let error = errors[0]; - assert.equal(error.path, 'SimpleEntitlements.xml'); + it('excludes diagnostics that are missing a path', () => { + sinon.stub(compiler as any, 'processMultiLineErrors').returns({}); + expect( + compiler.getErrors(['']) + ).to.eql([]); + }); + + describe('sendErrors', () => { + it('emits the errors', async () => { + 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++; + }); + let promise = compiler.sendErrors(); + clock.tick(1000); + await promise; + expect(callCount).to.eql(1); + + }); }); }); - describe('getMultipleFileXmlError ', () => { - it('tests no input', () => { - let input = ['']; - let errors = compiler.getMultipleFileXmlError(input); - assert.isEmpty(errors); + describe('parseGenericXmlError ', () => { + it('handles empty line', () => { + expect( + compiler.getErrors([``]) + ).to.eql([]); }); - it('tests no match', () => { - let input = ['some other output']; - let errors = compiler.getMultipleFileXmlError(input); - assert.isEmpty(errors); + it('handles non match', () => { + expect( + compiler.getErrors(['some other output']) + ).to.eql([]); }); - it('tests no match multiline', () => { - let input = [`multiline text`, `with no match`]; - let errors = compiler.getMultipleFileXmlError(input); - assert.isEmpty(errors); + it('handles multi-line non match no match multiline', () => { + expect( + compiler.getErrors([`multiline text`, `with no match`]) + ).to.eql([]); }); - it('match 1 file', () => { - let input = [`-------> Error parsing multiple XML components (SimpleEntitlements.xml)`]; - let errors = compiler.getMultipleFileXmlError(input); - assert.lengthOf(errors, 1); - let error = errors[0]; - assert.equal(error.path, 'SimpleEntitlements.xml'); + it('matches relative xml path', () => { + expect( + compiler.getErrors([`-------> Error parsing XML component SimpleButton.xml`]) + ).to.eql([{ + path: 'SimpleButton.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined, + severity: DiagnosticSeverity.Error + }]); }); - it('match 2 files', () => { - let input = [`-------> Error parsing multiple XML components (SimpleEntitlements.xml, Otherfile.xml)`]; - let errors = compiler.getMultipleFileXmlError(input); - assert.lengthOf(errors, 2); - let error = errors[0]; - assert.equal(error.path, 'SimpleEntitlements.xml'); + it('matches absolute xml path', () => { + expect( + compiler.getErrors([`-------> Error parsing XML component pkg:/components/SimpleButton.xml`]) + ).to.eql([{ + path: 'pkg:/components/SimpleButton.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined, + severity: DiagnosticSeverity.Error + }]); + }); + }); - let error2 = errors[1]; - assert.equal(error2.path, 'Otherfile.xml'); + it('handles when the next line is missing', () => { + expect( + compiler.getErrors([ + `Error in XML component RedButton defined in file pkg:/components/RedButton.xml` + //normally there's another line here, containing something like `-- Extends type does not exist: "ColoredButton"`. + //This test omits it on purpose to make sure we can still detect an error + ]) + ).to.eql([{ + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error in XML component RedButton', + path: 'pkg:/components/RedButton.xml', + code: undefined, + severity: DiagnosticSeverity.Error + }]); + }); + + describe('parseSyntaxAndCompileErrors', () => { + it('works with standard message', () => { + expect( + compiler.getErrors([`--- Invalid #If/#ElseIf expression ( not defined) (compile error &h92) in Parsers.brs(19) 'BAD_BS_CONST'`]) + ).to.eql([{ + path: 'Parsers.brs', + range: bscUtil.createRange(18, 0, 18, 999), + message: `Invalid #If/#ElseIf expression ( not defined) 'BAD_BS_CONST'`, + code: '&h92', + severity: DiagnosticSeverity.Error + }]); }); - it('match 2 files amongst other stuff', () => { - let input = [ - `some other output`, - `some other output2`, - `-------> Error parsing multiple XML components (SimpleEntitlements.xml, Otherfile.xml)`, - `some other output3` - ]; - let errors = compiler.getMultipleFileXmlError(input); - assert.lengthOf(errors, 2); - let error = errors[0]; - assert.equal(error.path, 'SimpleEntitlements.xml'); + it('works with zero leading junk', () => { + expect( + compiler.getErrors([`Invalid #If/#ElseIf expression ( not defined) (compile error &h92) in Parsers.brs(19) 'BAD_BS_CONST'`]) + ).to.eql([{ + path: 'Parsers.brs', + range: bscUtil.createRange(18, 0, 18, 999), + message: `Invalid #If/#ElseIf expression ( not defined) 'BAD_BS_CONST'`, + code: '&h92', + severity: DiagnosticSeverity.Error + }]); + }); - let error2 = errors[1]; - assert.equal(error2.path, 'Otherfile.xml'); + it('works when missing trailing context', () => { + expect( + compiler.getErrors([`--- Invalid #If/#ElseIf expression ( not defined) (compile error &h92) in Parsers.brs(19)`]) + ).to.eql([{ + path: 'Parsers.brs', + range: bscUtil.createRange(18, 0, 18, 999), + message: `Invalid #If/#ElseIf expression ( not defined)`, + code: '&h92', + severity: DiagnosticSeverity.Error + }]); + }); + + it('works when missing line number', () => { + expect( + compiler.getErrors([`--- Invalid #If/#ElseIf expression ( not defined) (compile error &h92) in Parsers.brs() 'BAD_BS_CONST'`]) + ).to.eql([{ + path: 'Parsers.brs', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Invalid #If/#ElseIf expression ( not defined) 'BAD_BS_CONST'`, + code: '&h92', + severity: DiagnosticSeverity.Error + }]); + }); + + it('works when missing error code', () => { + expect( + compiler.getErrors([`--- Invalid #If/#ElseIf expression ( not defined) (compile error ) in Parsers.brs(19) 'BAD_BS_CONST'`]) + ).to.eql([{ + path: 'Parsers.brs', + range: bscUtil.createRange(18, 0, 18, 999), + message: `Invalid #If/#ElseIf expression ( not defined) 'BAD_BS_CONST'`, + code: undefined, + severity: DiagnosticSeverity.Error + }]); }); }); + describe('getMultipleFileXmlError ', () => { + it('matches 1 relative file', () => { + expect( + compiler.getErrors([`-------> Error parsing multiple XML components (SimpleEntitlements.xml)`]) + ).to.eql([{ + path: 'SimpleEntitlements.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined, + severity: DiagnosticSeverity.Error + }]); + }); + + it('matches 2 relative files', () => { + expect( + compiler.getErrors([`-------> Error parsing multiple XML components (SimpleEntitlements.xml, Otherfile.xml)`]) + ).to.eql([{ + path: 'SimpleEntitlements.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined, + severity: DiagnosticSeverity.Error + }, { + path: 'Otherfile.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined, + severity: DiagnosticSeverity.Error + }]); + }); + + it('matches 1 absolute file', () => { + expect( + compiler.getErrors([`-------> Error parsing multiple XML components (pkg:/components/SimpleEntitlements.xml)`]) + ).to.eql([{ + path: 'pkg:/components/SimpleEntitlements.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined, + severity: DiagnosticSeverity.Error + }]); + }); + + it('matches 2 absolute files', () => { + expect( + compiler.getErrors([`-------> Error parsing multiple XML components (pkg:/components/SimpleEntitlements.xml, pkg:/components/Otherfile.xml)`]) + ).to.eql([{ + path: 'pkg:/components/SimpleEntitlements.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined, + severity: DiagnosticSeverity.Error + }, { + path: 'pkg:/components/Otherfile.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined, + severity: DiagnosticSeverity.Error + }]); + }); + + it('match 2 files amongst other stuff', () => { + expect( + compiler.getErrors([ + `some other output`, + `some other output2`, + `-------> Error parsing multiple XML components (SimpleEntitlements.xml, Otherfile.xml)`, + `some other output3` + ]) + ).to.eql([{ + path: 'SimpleEntitlements.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined, + severity: DiagnosticSeverity.Error + }, { + path: 'Otherfile.xml', + range: bscUtil.createRange(0, 0, 0, 999), + message: `Error parsing XML component`, + code: undefined, + severity: DiagnosticSeverity.Error + }]); + }); + }); + + it('ignores livecompile errors', () => { + expect( + compiler.getErrors([ + `------ Compiling dev 'sampleApp' ------`, + `=================================================================`, + `Found 1 compile error in file tmp/plugin/IJBAAAfijvb8/pkg:/components/Scene/MainScene.GetConfigurationw.brs`, + `--- Error loading file. (compile error &hb9) in pkg:/components/Scene/MainScene.GetConfigurationw.brs`, + `A block (such as FOR/NEXT or IF/ENDIF) was not terminated correctly. (compile error &hb5) in $LIVECOMPILE(1190)`, + `BrightScript Debugger> while True` + ]) + ).to.eql([{ + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error loading file', + path: 'pkg:/components/Scene/MainScene.GetConfigurationw.brs', + code: '&hb9', + severity: DiagnosticSeverity.Error + }]); + }); + describe('processUnhandledLines', () => { - async function runTest(lines: string[], expectedStatus: CompileStatus, expectedErrors?: BrightScriptDebugCompileError[]) { - let compileErrors: BrightScriptDebugCompileError[]; + async function runTest(lines: string | string[], expectedStatus: CompileStatus, expectedErrors?: BSDebugDiagnostic[]) { + let compileErrors: BSDebugDiagnostic[]; let promise: Promise; if (expectedErrors) { promise = new Promise((resolve) => { - compiler.on('compile-errors', (errors) => { + compiler.on('diagnostics', (errors) => { compileErrors = errors; resolve(); }); }); } - 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 @@ -127,6 +362,94 @@ describe('BrightScriptDebugger', () => { 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)`, @@ -170,20 +493,66 @@ describe('BrightScriptDebugger', () => { `-------> Compilation Failed.` ]; - let expectedErrors = [{ - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 1, - message: 'Found 1 compile error in file tmp/plugin/IJBAAAfijvb8/pkg:/components/Scene/MainScene.GetConfigurationw.brs\n--- Error loading file. (compile error &hb9) in pkg:/components/Scene/MainScene.GetConfigurationw.brs', - path: 'pkg:/components/Scene/MainScene.GetConfigurationw.brs' - }]; + await runTest(lines, CompileStatus.compileError, [{ + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error loading file', + path: 'pkg:/components/Scene/MainScene.GetConfigurationw.brs', + code: '&hb9', + severity: DiagnosticSeverity.Error + }]); + }); - await runTest(lines, CompileStatus.compileError, expectedErrors); + it('detects multi-line syntax errors', async () => { + await runTest([ + `08-25 19:03:56.531 [beacon.signal] |AppLaunchInitiate ---------> TimeBase(0 ms)`, + `08-25 19:03:56.531 [beacon.signal] |AppCompileInitiate --------> TimeBase(0 ms)`, + `08-25 19:03:56.531 [scrpt.cmpl] Compiling 'Hello World Console 2', id 'dev'`, + `08-25 19:03:56.532 [scrpt.load.mkup] Loading markup dev 'Hello World Console 2'`, + `08-25 19:03:56.532 [scrpt.unload.mkup] Unloading markup dev 'Hello World Console 2'`, + `=================================================================`, + `Found 3 parse errors in XML file Foo.xml`, + `--- Line 2: Unexpected data found inside a element (first 10 characters are "aaa")`, + `--- Line 3: Some unique error message`, + `--- Line 5: message with Line 4 inside it`, + `08-25 19:03:56.536 [scrpt.parse.mkup.time] Parsed markup dev 'Hello World Console 2' in 4 milliseconds`, + `------ Compiling dev 'Hello World Console 2' ------`, + `BRIGHTSCRIPT: WARNING: unused variable 'person' in function 'main' in #130`, + `BRIGHTSCRIPT: WARNING: unused variable 'arg1' in function 'noop' in #131`, + `Displayed 2 of 2 warnings`, + `08-25 19:03:56.566 [scrpt.ctx.cmpl.time] Compiled 'Hello World Console 2', id 'dev' in 29 milliseconds (BCVer:0)`, + `08-25 19:03:56.567 [scrpt.unload.mkup] Unloading markup dev 'Hello World Console 2'`, + `=================================================================`, + `An error occurred while attempting to compile the application's components:`, + `-------> Error parsing XML component Foo.xml` + ], CompileStatus.compileError, [{ + range: bscUtil.createRange(1, 0, 1, 999), + message: 'Unexpected data found inside a element (first 10 characters are "aaa")', + path: 'Foo.xml', + code: undefined, + severity: DiagnosticSeverity.Error + }, { + range: bscUtil.createRange(2, 0, 2, 999), + message: 'Some unique error message', + path: 'Foo.xml', + code: undefined, + severity: DiagnosticSeverity.Error + }, { + range: bscUtil.createRange(4, 0, 4, 999), + message: 'message with Line 4 inside it', + path: 'Foo.xml', + code: undefined, + severity: DiagnosticSeverity.Error + }, { + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error parsing XML component', + path: 'Foo.xml', + code: undefined, + severity: DiagnosticSeverity.Error + }]); }); it('detects XML syntax error', async () => { - let lines = [ + await runTest([ `03-26 22:21:46.570 [beacon.signal] |AppLaunchInitiate ---------> TimeBase(0)`, `03-26 22:21:46.571 [beacon.signal] |AppCompileInitiate --------> TimeBase(1 ms)`, `03-26 22:21:46.571 [scrpt.cmpl] Compiling 'sampleApp', id 'dev'`, @@ -205,27 +574,21 @@ describe('BrightScriptDebugger', () => { `-------> Error parsing XML component SampleScreen.xml`, ``, `[RAF] Roku_Ads Framework version 2.1231` - ]; - - let expectedErrors = [ + ], CompileStatus.compileError, [ { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 3, + range: bscUtil.createRange(2, 0, 2, 999), message: 'XML syntax error found ---> not well-formed (invalid token)', - path: 'SampleScreen.xml' + path: 'SampleScreen.xml', + code: undefined, + severity: DiagnosticSeverity.Error }, { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 1, - message: 'General XML compilation error', - path: 'SampleScreen.xml' + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error parsing XML component', + path: 'SampleScreen.xml', + code: undefined, + severity: DiagnosticSeverity.Error } - ]; - - await runTest(lines, CompileStatus.compileError, expectedErrors); + ]); }); it('detects BRS syntax error', async () => { @@ -247,46 +610,39 @@ describe('BrightScriptDebugger', () => { `--- Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(734)` ]; - let expectedErrors = [ + await runTest(lines, CompileStatus.compileError, [ { - charEnd: 999, - charStart: 0, - lineNumber: 595, - errorText: '--- Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(595)', - message: 'Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(595)', - path: 'pkg:/components/Services/Network/Parsers.brs' + range: bscUtil.createRange(595 - 1, 0, 595 - 1, 999), + code: '&h02', + message: 'Syntax Error', + path: 'pkg:/components/Services/Network/Parsers.brs', + severity: DiagnosticSeverity.Error }, { - charEnd: 999, - charStart: 0, - lineNumber: 598, - errorText: '--- Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(598)', - message: 'Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(598)', - path: 'pkg:/components/Services/Network/Parsers.brs' + range: bscUtil.createRange(598 - 1, 0, 598 - 1, 999), + code: '&h02', + message: 'Syntax Error', + path: 'pkg:/components/Services/Network/Parsers.brs', + severity: DiagnosticSeverity.Error }, { - charEnd: 999, - charStart: 0, - lineNumber: 732, - errorText: '--- Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(732)', - message: 'Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(732)', - path: 'pkg:/components/Services/Network/Parsers.brs' + range: bscUtil.createRange(732 - 1, 0, 732 - 1, 999), + code: '&h02', + message: 'Syntax Error', + path: 'pkg:/components/Services/Network/Parsers.brs', + severity: DiagnosticSeverity.Error }, { - charEnd: 999, - charStart: 0, - lineNumber: 733, - errorText: '--- Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(733)', - message: 'Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(733)', - path: 'pkg:/components/Services/Network/Parsers.brs' + range: bscUtil.createRange(733 - 1, 0, 733 - 1, 999), + code: '&h02', + message: 'Syntax Error', + path: 'pkg:/components/Services/Network/Parsers.brs', + severity: DiagnosticSeverity.Error }, { - charEnd: 999, - charStart: 0, - lineNumber: 734, - errorText: '--- Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(734)', - message: 'Syntax Error. (compile error &h02) in pkg:/components/Services/Network/Parsers.brs(734)', - path: 'pkg:/components/Services/Network/Parsers.brs' + range: bscUtil.createRange(734 - 1, 0, 734 - 1, 999), + code: '&h02', + message: 'Syntax Error', + path: 'pkg:/components/Services/Network/Parsers.brs', + severity: DiagnosticSeverity.Error } - ]; - - await runTest(lines, CompileStatus.compileError, expectedErrors); + ]); }); it('detects Multiple XML syntax errors', async () => { @@ -302,8 +658,8 @@ describe('BrightScriptDebugger', () => { `--- Line 3: XML syntax error found ---> not well-formed (invalid token)`, ``, `=================================================================`, - `Error in XML component Oops defined in file pkg:/components/Oops.xml`, - `-- Extends type does not exist: "BaseOops"`, + `Error in XML component RedButton defined in file pkg:/components/RedButton.xml`, + `-- Extends type does not exist: "ColoredButton"`, ``, `=================================================================`, `Found 1 parse error in XML file ChannelItemComponent.xml`, @@ -316,49 +672,48 @@ describe('BrightScriptDebugger', () => { ``, `=================================================================`, `An error occurred while attempting to compile the application's components:`, - `-------> Error parsing multiple XML components (SampleScreen.xml, ChannelItemComponent.xml, Ooops)` + `-------> Error parsing multiple XML components (SampleScreen.xml, ChannelItemComponent.xml, RedButton.xml)` ]; - let expectedErrors = [ + await runTest(lines, CompileStatus.compileError, [ { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 3, + range: bscUtil.createRange(2, 0, 2, 999), message: 'XML syntax error found ---> not well-formed (invalid token)', - path: 'SampleScreen.xml' + path: 'SampleScreen.xml', + code: undefined, + severity: DiagnosticSeverity.Error }, { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 9, + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Extends type does not exist: "ColoredButton"', + path: 'pkg:/components/RedButton.xml', + 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' + path: 'ChannelItemComponent.xml', + code: undefined, + severity: DiagnosticSeverity.Error }, { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 1, - message: 'General XML compilation error', - path: 'SampleScreen.xml' + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error parsing XML component', + path: 'SampleScreen.xml', + code: undefined, + severity: DiagnosticSeverity.Error }, { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 1, - message: 'General XML compilation error', - path: 'ChannelItemComponent.xml' + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error parsing XML component', + path: 'ChannelItemComponent.xml', + code: undefined, + severity: DiagnosticSeverity.Error }, { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 1, - message: 'Error in XML component Oops defined in file pkg:/components/Oops.xml\n-- Extends type does not exist: "BaseOops"', - path: 'pkg:/components/Oops.xml' + range: bscUtil.createRange(0, 0, 0, 999), + message: 'Error parsing XML component', + path: 'RedButton.xml', + code: undefined, + severity: DiagnosticSeverity.Error } - ]; - - await runTest(lines, CompileStatus.compileError, expectedErrors); + ]); }); it('detects Invalid #If/#ElseIf expression', async () => { @@ -377,22 +732,19 @@ describe('BrightScriptDebugger', () => { `--- Invalid #If/#ElseIf expression ( not defined) (compile error &h92) in Parsers.brs(19) 'BAD_BS_CONST'` ]; - let expectedErrors = [ + await runTest(lines, CompileStatus.compileError, [ { - charEnd: 999, - charStart: 0, - errorText: '--- Invalid #If/#ElseIf expression ( not defined) (compile error &h92) in Parsers.brs(19) \'BAD_BS_CONST\'', - lineNumber: 19, - message: 'compile error &h92) in Parsers.brs(19) \'BAD_BS_CONST\'', - path: 'Parsers.brs' + code: '&h92', + range: bscUtil.createRange(19 - 1, 0, 19 - 1, 999), + message: `Invalid #If/#ElseIf expression ( not defined) 'BAD_BS_CONST'`, + path: 'Parsers.brs', + severity: DiagnosticSeverity.Error } - ]; - - await runTest(lines, CompileStatus.compileError, expectedErrors); + ]); }); it('detects No manifest', async () => { - let lines = [ + await runTest([ `03-27 00:19:07.768 [beacon.signal] |AppLaunchInitiate ---------> TimeBase(0)`, `03-27 00:19:07.768 [beacon.signal] |AppCompileInitiate --------> TimeBase(0 ms)`, `03-27 00:19:07.768 [scrpt.cmpl] Compiling 'sampleApp', id 'dev'`, @@ -406,19 +758,15 @@ describe('BrightScriptDebugger', () => { `An error occurred while attempting to install the application:`, ``, `------->No manifest. Invalid package.` - ]; - - let expectedErrors = [ + ], CompileStatus.compileError, [ { - charEnd: 999, - charStart: 0, - errorText: 'ERR_COMPILE:', - lineNumber: 1, - message: 'No manifest. Invalid package.', - path: 'manifest' + range: bscUtil.createRange(0, 0, 0, 999), + message: 'No manifest. Invalid package', + path: 'pkg:/manifest', + severity: DiagnosticSeverity.Error, + code: undefined } - ]; - await runTest(lines, CompileStatus.compileError, expectedErrors); + ]); }); }); diff --git a/src/CompileErrorProcessor.ts b/src/CompileErrorProcessor.ts index de933a9a..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'; - -export const GENERAL_XML_ERROR = 'General XML compilation error'; +import { DiagnosticSeverity, util as bscUtil } from 'brighterscript'; export class CompileErrorProcessor { @@ -14,7 +14,7 @@ export class CompileErrorProcessor { private emitter = new EventEmitter(); public compileErrorTimer: NodeJS.Timeout; - public on(eventName: 'compile-errors', handler: (params: BrightScriptDebugCompileError[]) => void); + public on(eventName: 'diagnostics', handler: (params: BSDebugDiagnostic[]) => void); public on(eventName: string, handler: (payload: any) => void) { this.emitter.on(eventName, handler); return () => { @@ -24,56 +24,49 @@ export class CompileErrorProcessor { }; } - private emit(eventName: 'compile-errors', data?) { + private emit(eventName: 'diagnostics', 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 - 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; + } } } @@ -87,229 +80,242 @@ export class CompileErrorProcessor { }); } - private getErrors() { - return [ - ...this.getSyntaxErrors(this.compilingLines), - ...this.getCompileErrors(this.compilingLines), - ...this.getMultipleFileXmlError(this.compilingLines), - ...this.getSingleFileXmlError(this.compilingLines), - ...this.getSingleFileXmlComponentError(this.compilingLines), - ...this.getMissingManifestError(this.compilingLines) - ]; + public getErrors(lines: string[]) { + let diagnostics: BSDebugDiagnostic[] = []; + //clone the lines so the parsers can manipulate them + lines = [...lines]; + while (lines.length > 0) { + const startLength = lines.length; + const line = lines[0]; + + if (line) { + diagnostics.push( + ...[ + this.processMultiLineErrors(lines), + this.parseComponentDefinedInFileError(lines), + this.parseGenericXmlError(line), + this.parseSyntaxAndCompileErrors(line), + this.parseMissingManifestError(line) + ].flat().filter(x => !!x) + ); + } + //if none of the parsers consumed a line, remove the first line + if (lines.length === startLength) { + lines.shift(); + } + } + //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]); } /** - * Runs a regex to get the content between telnet commands - * @param value + * Parse generic xml errors with no further context below */ - private getSyntaxErrorDetails(value: string) { - return /(syntax|compile) error.* in (.*)\((\d+)\)(.*)/gim.exec(value); + public parseGenericXmlError(line: string): BSDebugDiagnostic[] { + let [, message, files] = this.execAndTrim( + // https://regex101.com/r/LDUyww/3 + /^(?:-+\>)?\s*(Error parsing (?:multiple )?XML component[s]?)\s+\(?(.+\.xml)\)?.*$/igm, + line + ) ?? []; + if (message && typeof files === 'string') { + //use the singular xml parse message since the plural doesn't make much sense when attached to a single file + if (message.toLowerCase() === 'error parsing multiple xml components') { + message = 'Error parsing XML component'; + } + //there can be 1 or more file paths, so add a distinct error for each one + return files.split(',') + .map(filePath => ({ + path: this.sanitizeCompilePath(filePath), + range: bscUtil.createRange(0, 0, 0, 999), + message: this.buildMessage(message), + code: undefined, + severity: DiagnosticSeverity.Error + })) + .filter(x => !!x); + } } - public getSyntaxErrors(lines: string[]): BrightScriptDebugCompileError[] { - let errors: BrightScriptDebugCompileError[] = []; - let match: RegExpExecArray; - // let syntaxRegEx = /(syntax|compile) error.* in (.*)\((\d+)\)/gim; - for (const line of lines) { - match = this.getSyntaxErrorDetails(line); - if (match) { - let path = this.sanitizeCompilePath(match[2]); - let lineNumber = parseInt(match[3]); //1-based - - //FIXME - //if this match is a livecompile error, throw out all prior errors because that means we are re-running - if (!path.toLowerCase().includes('$livecompile')) { + /** + * Parse the standard syntax and compile error format + */ + private parseSyntaxAndCompileErrors(line: string): BSDebugDiagnostic[] { + let [, message, errorType, code, trailingInfo] = this.execAndTrim( + // https://regex101.com/r/HHZ6dE/3 + /(.*?)(?:\(((?:syntax|compile)\s+error)\s+(&h[\w\d]+)?\s*\))\s*in\b\s+(.+)/ig, + line + ) ?? []; + + if (message) { + //split the file path, line number, and trailing context if available. + let [, filePath, lineNumber, context] = this.execAndTrim( + /(.+)\((\d+)?\)(.*)/ig, + trailingInfo + //default the `filePath` var to the whole `trailingInfo` string + ) ?? [null, trailingInfo, null, null]; + + return [{ + path: this.sanitizeCompilePath(filePath), + message: this.buildMessage(message, context), + range: this.getRange(lineNumber), //lineNumber is 1-based + code: code, + severity: DiagnosticSeverity.Error + }]; + } + } + /** + * Handles when an error lists the filename on the first line, then subsequent lines each have 1 error. + * Stops on the first line that doesn't have an error line. Like this: + * ``` + * Found 3 parse errors in XML file Foo.xml + * --- Line 2: Unexpected data found inside a element (first 10 characters are "aaa") + * --- Line 3: Some unique error message + * --- Line 5: message with Line 4 inside it + */ + private processMultiLineErrors(lines: string[]): BSDebugDiagnostic[] { + const errors = []; + let [, count, filePath] = this.execAndTrim( + // https://regex101.com/r/wBMp8B/1 + /found (\d+).*error[s]? in.*?file(.*)/gmi, + lines[0] + ) ?? []; + filePath = this.sanitizeCompilePath(filePath); + if (filePath) { + let i = 0; + //parse each line that looks like it's an error. + for (i = 1; i < lines.length; i++) { + //example: `Line 1: Unexpected data found inside a element (first 10 characters are "aaa")`) + const [, lineNumber, message] = this.execAndTrim( + /^[\-\s]*line (\d*):(.*)$/gim, + lines[i] + ) ?? []; + if (lineNumber && message) { errors.push({ - path: path, - lineNumber: lineNumber, - errorText: line, - message: match[0].trim(), - charStart: 0, - charEnd: 999 //TODO + path: filePath, + range: this.getRange(lineNumber), //lineNumber is 1-based + message: this.buildMessage(message), + code: undefined, + severity: DiagnosticSeverity.Error }); + } else { + //assume there are no more errors for this file + break; } } + //remove the lines we consumed + lines.splice(0, i); } return errors; } - public getCompileErrors(lines: string[]): BrightScriptDebugCompileError[] { - let errors: BrightScriptDebugCompileError[] = []; - let responseText = lines.join('\n'); - const filesWithErrors = responseText.split('================================================================='); - if (filesWithErrors.length < 2) { - return []; - } - - let getFileInfoRegEx = /Found(?:.*)file (.*)$/im; - for (let index = 1; index < filesWithErrors.length - 1; index++) { - const fileErrorText = filesWithErrors[index]; - //TODO - for now just a simple parse - later on someone can improve with proper line checks + all parse/compile types - //don't have time to do this now; just doing what keeps me productive. - let match = getFileInfoRegEx.exec(fileErrorText); - if (!match) { - continue; - } - - let path = this.sanitizeCompilePath(match[1]); - let lineNumber = 1; //TODO this should iterate over all line numbers found in a file - let errorText = 'ERR_COMPILE:'; - let message = fileErrorText.trim(); - - let error = { - path: path, - lineNumber: lineNumber, - errorText: errorText, - message: message, - charStart: 0, - charEnd: 999 //TODO - }; - - //now iterate over the lines, to see if there's any errors we can extract - let lineErrors = this.getLineErrors(path, fileErrorText); - if (lineErrors.length > 0) { - errors.push(...lineErrors); - } else { - errors.push(error); + /** + * Parse errors that look like this: + * ``` + * Error in XML component RedButton defined in file pkg:/components/RedButton.xml + * -- Extends type does not exist: "ColoredButton" + */ + private parseComponentDefinedInFileError(lines: string[]): BSDebugDiagnostic[] { + let [, message, filePath] = this.execAndTrim( + /(Error in XML component [a-z0-9_-]+) defined in file (.*)/i, + lines[0] + ) ?? []; + if (filePath) { + lines.shift(); + //assume the next line includes the actual error message + if (lines[0]) { + message = lines.shift(); } + return [{ + message: this.buildMessage(message), + path: this.sanitizeCompilePath(filePath), + range: this.getRange(), + code: undefined, + severity: DiagnosticSeverity.Error + }]; } - return errors; } - public getLineErrors(path: string, fileErrorText: string): BrightScriptDebugCompileError[] { - let errors: BrightScriptDebugCompileError[] = []; - let getFileInfoRegEx = /^--- Line (\d*): (.*)$/gim; - let match: RegExpExecArray; - // eslint-disable-next-line no-cond-assign - while (match = getFileInfoRegEx.exec(fileErrorText)) { - let lineNumber = parseInt(match[1]); // 1-based - let errorText = 'ERR_COMPILE:'; - let message = this.sanitizeCompilePath(match[2]); - - errors.push({ - path: path, - lineNumber: lineNumber, - errorText: errorText, - message: message, - charStart: 0, - charEnd: 999 //TODO - }); + /** + * Parse error messages that look like this: + * ``` + * ------->No manifest. Invalid package. + * ``` + */ + private parseMissingManifestError(line: string): BSDebugDiagnostic[] { + let [, message] = this.execAndTrim( + // https://regex101.com/r/ANr5xd/1 + /^(?:-+)>(No manifest\. Invalid package\.)/i + , + line + ) ?? []; + if (message) { + return [{ + path: 'pkg:/manifest', + range: bscUtil.createRange(0, 0, 0, 999), + message: this.buildMessage(message), + code: undefined, + severity: DiagnosticSeverity.Error + }]; } - - return errors; } - public getSingleFileXmlError(lines: string[]): BrightScriptDebugCompileError[] { - let errors: BrightScriptDebugCompileError[] = []; - let getFileInfoRegEx = /^-------> Error parsing XML component (.*).*$/i; - for (let line of lines) { - let match = getFileInfoRegEx.exec(line); - if (match) { - let errorText = 'ERR_COMPILE:'; - let path = this.sanitizeCompilePath(match[1]); - - errors.push({ - path: path, - lineNumber: 1, - errorText: errorText, - message: GENERAL_XML_ERROR, - charStart: 0, - charEnd: 999 //TODO - }); - } - } - - return errors; + /** + * Exec the regexp, and if there's a match, trim every group + */ + private execAndTrim(pattern: RegExp, text: string) { + return pattern.exec(text)?.map(x => x?.trim()); } - public getSingleFileXmlComponentError(lines: string[]): BrightScriptDebugCompileError[] { - let errors: BrightScriptDebugCompileError[] = []; - let getFileInfoRegEx = /Error in XML component [a-z0-9_-]+ defined in file (.*)/i; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - let match = getFileInfoRegEx.exec(line); - if (match) { - let errorText = 'ERR_COMPILE:'; - let path = match[1]; - errors.push({ - path: path, - lineNumber: 1, - errorText: errorText, - message: `${line}\n${lines[i + 1] ?? ''}`, - charStart: 0, - charEnd: 999 //TODO - }); - } - } - return errors; - } + private buildMessage(message: string, context?: string) { + //remove any leading dashes or whitespace + message = message.replace(/^[ \t\-]+/g, ''); - public getMultipleFileXmlError(lines: string[]): BrightScriptDebugCompileError[] { - let errors: BrightScriptDebugCompileError[] = []; - let getFileInfoRegEx = /^-------> Error parsing multiple XML components \((.*)\)/i; - for (const line of lines) { - let match = getFileInfoRegEx.exec(line); - if (match) { - let errorText = 'ERR_COMPILE:'; - let filePaths = match[1].split(','); - for (const path of filePaths) { - errors.push({ - path: this.sanitizeCompilePath(path.trim()), - lineNumber: 1, - errorText: errorText, - message: GENERAL_XML_ERROR, - charStart: 0, - charEnd: 999 //TODO - }); - } - } + //append context to end of message (if available) + if (context?.length > 0) { + message += ' ' + context; } + //remove trailing period from message + message = message.replace(/[\s.]+$/, ''); - return errors; + return message; } - public getMissingManifestError(lines: string[]): BrightScriptDebugCompileError[] { - let errors: BrightScriptDebugCompileError[] = []; - let getMissingManifestErrorRegEx = /^(?:-+)>(No manifest\. Invalid package\.)/i; - for (const line of lines) { - let match = getMissingManifestErrorRegEx.exec(line); - if (match) { - errors.push({ - path: 'manifest', - lineNumber: 1, - errorText: 'ERR_COMPILE:', - message: match[1], - charStart: 0, - charEnd: 999 //TODO - }); - } - } - - return errors; + /** + * Given a text-based line number, convert it to a number and return a range. + * Defaults to line number 1 (1-based) if unable to parse. + * @returns a zero-based vscode `Range` object + */ + private getRange(lineNumberText?: string) { + //convert the line number to an integer (if applicable) + let lineNumber = parseInt(lineNumberText); //1-based + lineNumber = isNaN(lineNumber) ? 1 : lineNumber; + return bscUtil.createRange(lineNumber - 1, 0, lineNumber - 1, 999); } + /** + * Trim all leading junk up to the `pkg:/` in this string + */ public sanitizeCompilePath(debuggerPath: string): string { - let protocolIndex = debuggerPath.indexOf('pkg:/'); - - if (protocolIndex > 0) { - return debuggerPath.slice(protocolIndex); - } - - return debuggerPath; + return debuggerPath?.replace(/.*?(?=pkg:\/)/, '')?.trim(); } public resetCompileErrorTimer(isRunning): any { - // console.debug('resetCompileErrorTimer isRunning' + isRunning); - if (this.compileErrorTimer) { - clearInterval(this.compileErrorTimer); + clearTimeout(this.compileErrorTimer); this.compileErrorTimer = undefined; } if (isRunning) { if (this.status === CompileStatus.compileError) { - // console.debug('resetting resetCompileErrorTimer'); this.compileErrorTimer = setTimeout(() => { this.onCompileErrorTimer(); }, this.compileErrorTimeoutMs); @@ -318,36 +324,21 @@ export class CompileErrorProcessor { } public onCompileErrorTimer() { - console.debug('onCompileErrorTimer: timer complete. should\'ve caught all errors '); - this.status = CompileStatus.compileError; this.resetCompileErrorTimer(false); 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); } /** @@ -355,26 +346,34 @@ export class CompileErrorProcessor { * @param responseText */ private reportErrors() { - console.debug('reportErrors'); - - const errors = this.getErrors().filter((e) => { - const path = e.path.toLowerCase(); - return path.endsWith('.brs') || path.endsWith('.xml') || path === 'manifest'; - }); - + const errors = this.getErrors(this.compilingLines); if (errors.length > 0) { - this.emit('compile-errors', errors); + this.emit('diagnostics', errors); + } + } + + public destroy() { + if (this.emitter) { + this.emitter.removeAllListeners(); } } } -export interface BrightScriptDebugCompileError { +export interface BSDebugDiagnostic extends Diagnostic { + /** + * Path to the file in question. When emitted from a Roku device, this will be a full pkgPath (i.e. `pkg:/source/main.brs`). + * As it flows through the program, this may be modified to represent a source location (i.e. `C:/projects/app/source/main.brs`) + */ path: string; - lineNumber: number; - message: string; - errorText: string; - charStart: number; - charEnd: number; + /** + * The name of the component library this diagnostic was emitted from. Should be undefined if diagnostic originated from the + * main app. + */ + componentLibraryName?: string; + /** + * The diagnostic's severity. + */ + severity: DiagnosticSeverity; } export enum CompileStatus { diff --git a/src/FileUtils.spec.ts b/src/FileUtils.spec.ts index 2f882c9d..4ee5ce32 100644 --- a/src/FileUtils.spec.ts +++ b/src/FileUtils.spec.ts @@ -45,6 +45,14 @@ describe('FileUtils', () => { it('removes leading ... when present', () => { expect(fileUtils.removeFileTruncation('...project1/main.brs')).to.equal('project1/main.brs'); }); + + it('removes leading .../', () => { + expect(fileUtils.removeFileTruncation('.../project1/main.brs')).to.equal('project1/main.brs'); + }); + + it('removes leading /', () => { + expect(fileUtils.removeFileTruncation('/project1/main.brs')).to.equal('project1/main.brs'); + }); }); describe('pathEndsWith', () => { @@ -269,4 +277,28 @@ describe('FileUtils', () => { expect(fileUtils.removeTrailingSlash('a')).to.equal('a'); }); }); + + describe('unPostfixFilePath', () => { + it('removes postfix from paths that contain it', () => { + expect(fileUtils.unPostfixFilePath(`source/main__lib1.brs`, '__lib1')).to.equal('source/main.brs'); + expect(fileUtils.unPostfixFilePath(`components/component1__lib1.brs`, '__lib1')).to.equal('components/component1.brs'); + }); + + it('removes postfix case insensitive', () => { + expect(fileUtils.unPostfixFilePath(`source/main__LIB1.brs`, '__lib1')).to.equal('source/main.brs'); + expect(fileUtils.unPostfixFilePath(`source/MAIN__lib1.brs`, '__lib1')).to.equal('source/MAIN.brs'); + }); + + it('does nothing to files without the postfix', () => { + expect(fileUtils.unPostfixFilePath(`source/main.brs`, '__lib1')).to.equal('source/main.brs'); + }); + + it('does nothing to files with a different postfix', () => { + expect(fileUtils.unPostfixFilePath(`source/main__lib1.brs`, '__lib0')).to.equal('source/main__lib1.brs'); + }); + + it('only removes the postfix from the end of the file', () => { + expect(fileUtils.unPostfixFilePath(`source/__lib1.brs/main.brs`, '__lib1')).to.equal('source/__lib1.brs/main.brs'); + }); + }); }); diff --git a/src/FileUtils.ts b/src/FileUtils.ts index efaa7b82..2b9a2974 100644 --- a/src/FileUtils.ts +++ b/src/FileUtils.ts @@ -97,11 +97,12 @@ export class FileUtils { } /** - * The Roku telnet debugger truncates file paths, so this removes that truncation piece. + * The Roku telnet debugger truncates file paths, so this removes that leading truncation piece. + * This removes 0 or more leading dots, followed by 0 or more leading slashes * @param filePath */ public removeFileTruncation(filePath: string) { - return filePath.startsWith('...') ? filePath.substring(3) : filePath; + return filePath.replace(/^\.*[\/\\]*/, ''); } /** @@ -170,6 +171,33 @@ export class FileUtils { return result; } + /** + * Append a postfix to the filename BEFORE its file extension + */ + public postfixFilePath(filePath: string, postfix: string, fileExtensions: string[]) { + let parsedPath = path.parse(filePath); + + if (fileExtensions.includes(parsedPath.ext)) { + const regexp = new RegExp(parsedPath.ext + '$', 'i'); + return filePath.replace(regexp, postfix + parsedPath.ext); + } else { + return filePath; + } + } + + /** + * Given a file path, return a new path with the component library postfix removed + */ + public unPostfixFilePath(filePath: string, postfix: string) { + let parts = path.parse(filePath); + const search = `${postfix}${parts.ext}`; + if (filePath.toLowerCase().endsWith(search.toLowerCase())) { + return fileUtils.replaceCaseInsensitive(filePath, search, parts.ext); + } else { + return filePath; + } + } + /** * Replace all directory separators with current OS separators, * force all drive letters to lower case (because that's what VSCode does sometimes so this makes it consistent) diff --git a/src/LaunchConfiguration.ts b/src/LaunchConfiguration.ts index 4e9ca8ae..839154a7 100644 --- a/src/LaunchConfiguration.ts +++ b/src/LaunchConfiguration.ts @@ -6,6 +6,10 @@ import type { LogLevel } from './logging'; * This interface should always match the schema found in the mock-debug extension manifest. */ export interface LaunchConfiguration extends DebugProtocol.LaunchRequestArguments { + /** + * The current working directory of the launcher. When running from vscode, this should be the value from `${workspaceFolder}` + */ + cwd: string; /** * The host or ip address for the target Roku */ @@ -43,7 +47,9 @@ export interface LaunchConfiguration extends DebugProtocol.LaunchRequestArgument bsConst?: Record; /** - * Port to access component libraries. + * Port used to use when spinning up a web server to host component libraries. This runs on the developer machine and does not correspond to a port on the Roku device. + * Defaults to 8080 + * @default 8080 */ componentLibrariesPort: number; @@ -79,6 +85,75 @@ export interface LaunchConfiguration extends DebugProtocol.LaunchRequestArgument */ consoleOutput: 'full' | 'normal'; + fileLogging?: boolean | { + /** + * Should file logging be enabled + */ + enabled?: boolean; + /** + * Directory where log files should be stored. used when filename is relative + */ + dir?: string; + /** + * The number of log files to keep. undefined or < 0 means keep all + */ + logLimit?: number; + /** + * File logging for the telnet or IO output from the Roku device currently being debugged. (i.e. all the stuff produced by `print` statements in your code) + */ + rokuDevice?: boolean | { + /** + * Should file logging be enabled for this logging type? + */ + enabled?: boolean; + /** + * Directory where log files should be stored. used when filename is relative + */ + dir?: string; + /** + * The name of the log file. When mode==='session', a datestamp will be prepended to this filename. + * Can be absolute or relative, and relative paths will be relative to `this.dir` + */ + filename?: string; + /** + * - 'session' means a unique timestamped file will be created on every debug session. + * - 'append' means all logs will be appended to a single file + */ + mode?: 'session' | 'append'; + /** + * The number of log files to keep. undefined or < 0 means keep all + */ + logLimit?: number; + }; + /** + * File logging for the debugger. Mostly used to provide crash logs to the RokuCommunity team. + */ + debugger?: boolean | { + /** + * Should file logging be enabled for this logging type? + */ + enabled?: boolean; + /** + * Directory where log files should be stored. used when filename is relative + */ + dir?: string; + /** + * The name of the log file. When mode==='session', a datestamp will be prepended to this filename. + * Can be absolute or relative, and relative paths will be relative to `this.dir` + */ + filename?: string; + /** + * - 'session' means a unique timestamped file will be created on every debug session. + * - 'append' means all logs will be appended to a single file + */ + mode?: 'session' | 'append'; + /** + * The number of log files to keep. undefined or < 0 means keep all + */ + logLimit?: number; + }; + }; + /** * If specified, the debug session will start the roku app using the deep link */ @@ -150,6 +225,7 @@ export interface LaunchConfiguration extends DebugProtocol.LaunchRequestArgument * The port that should be used when installing the package. Defaults to 80. * This is mainly useful for things like emulators that use alternate ports, * or when publishing through some type of port forwarding configuration. + * @default 80 */ packagePort?: number; @@ -157,24 +233,98 @@ export interface LaunchConfiguration extends DebugProtocol.LaunchRequestArgument * The port used to send remote control commands (like home press, back, etc.). Defaults to 8060. * This is mainly useful for things like emulators that use alternate ports, * or when sending commands through some type of port forwarding. + * @default 8060 */ remotePort?: number; /** - * The brightscript console port. In telnet mode this is the port used for the telnet connection. In debug protocol mode, this is used to obtain compile errors from the device. + * The port that should be used to send SceneGraph debug commands. Defaults to 8080. + * @default 8080 + */ + sceneGraphDebugCommandsPort: number; + + /** + * Port used to connect to and control a debug protocol session. + * This is mainly useful for things like emulators that use alternate ports, + * or when connecting to the debug protocol through some type of port forwarding. Defaults to 8081. + * @default 8081 + */ + controlPort: number; + + /** + * The brightscript console port. In telnet mode this is the port used for the primary telnet connection. In debug protocol mode, this is used to obtain compile errors from the device. Defaults to 8085. + * @default 8085 */ brightScriptConsolePort?: number; /** * The path used for the staging folder of roku-deploy * This should generally be set to "${cwd}/.roku-deploy-staging", but that's ultimately up to the debug client. + * @deprecated use `stagingDir` instead */ stagingFolderPath?: string; + /** + * Path used for the staging folder where files are written right before being packaged. This folder will contain any roku-debug related sourcemaps, + * as well as having breakpoints injected into source code + */ + stagingDir?: string; + /** * What level of debug server's internal logging should be performed in the debug session */ logLevel: LogLevel; + + /** + * Show variables that are prefixed with a special prefix designated to be hidden + */ + showHiddenVariables: boolean; + + /** + * If true: turn on ECP rendezvous tracking, or turn on 8080 rendezvous tracking if ECP unsupported + * If false, turn off both. + * @default true + */ + rendezvousTracking: boolean; + + /** + * Delete any currently installed dev channel before starting the debug session + * @default false + */ + deleteDevChannelBeforeInstall: boolean; + + /** + * Task to run instead of roku-deploy to produce the .zip file that will be uploaded to the Roku. + */ + packageTask: string; + + /** + * Path to the .zip that will be uploaded to the Roku + */ + packagePath: string; + + /** + * Overrides for values used during the roku-deploy zip upload process, like the route and various form data. You probably don't need to change these.. + */ + packageUploadOverrides?: { + /** + * The route to use for uploading to the Roku device. Defaults to 'plugin_install' + * @default 'plugin_install' + */ + route: string; + + /** + * A dictionary of form fields to be included in the package upload request. Set a value to null to delete from the form + */ + formData: Record; + }; + + /** + * Should the ChannelPublishedEvent be emitted. This is a hack for when certain roku devices become locked up as a result of this event + * being emitted. You probably don't need to set this + * @default true + */ + emitChannelPublishedEvent?: boolean; } export interface ComponentLibraryConfiguration { diff --git a/src/RendezvousTracker.spec.ts b/src/RendezvousTracker.spec.ts index b0efb228..997b21ff 100644 --- a/src/RendezvousTracker.spec.ts +++ b/src/RendezvousTracker.spec.ts @@ -1,7 +1,10 @@ -import * as sinon from 'sinon'; +import { createSandbox } from 'sinon'; +const sinon = createSandbox(); import { assert, expect } from 'chai'; import type { RendezvousHistory } from './RendezvousTracker'; import { RendezvousTracker } from './RendezvousTracker'; +import { SceneGraphDebugCommandController } from './SceneGraphDebugCommandController'; +import type { LaunchConfiguration } from './LaunchConfiguration'; describe('BrightScriptFileUtils ', () => { let rendezvousTracker: RendezvousTracker; @@ -10,7 +13,14 @@ describe('BrightScriptFileUtils ', () => { let expectedHistory: RendezvousHistory; beforeEach(() => { - rendezvousTracker = new RendezvousTracker(); + let launchConfig = { + 'host': '192.168.1.5', + 'remotePort': 8060 + }; + let deviceInfo = { + softwareVersion: '11.5.0' + }; + rendezvousTracker = new RendezvousTracker(deviceInfo, launchConfig as any); rendezvousTracker.registerSourceLocator(async (debuggerPath: string, lineNumber: number) => { //remove preceding pkg: if (debuggerPath.toLowerCase().startsWith('pkg:')) { @@ -259,13 +269,160 @@ describe('BrightScriptFileUtils ', () => { }); - afterEach(() => { + afterEach(async () => { + sinon.restore(); rendezvousTrackerMock.restore(); + + //prevent hitting the network during teardown + rendezvousTracker.toggleEcpRendezvousTracking = () => Promise.resolve() as any; + rendezvousTracker['runSGLogrendezvousCommand'] = () => Promise.resolve() as any; + + await rendezvousTracker?.destroy(); + }); + + describe('isEcpRendezvousTrackingSupported ', () => { + it('works', () => { + rendezvousTracker['deviceInfo'].softwareVersion = '11.0.0'; + expect(rendezvousTracker.doesHostSupportEcpRendezvousTracking).to.be.false; + + rendezvousTracker['deviceInfo'].softwareVersion = '11.5.0'; + expect(rendezvousTracker.doesHostSupportEcpRendezvousTracking).to.be.true; + + rendezvousTracker['deviceInfo'].softwareVersion = '12.0.1'; + expect(rendezvousTracker.doesHostSupportEcpRendezvousTracking).to.be.true; + }); + + it('does not crash when softwareVersion is corrupt or missing', () => { + rendezvousTracker['deviceInfo'].softwareVersion = ''; + expect(rendezvousTracker.doesHostSupportEcpRendezvousTracking).to.be.false; + + rendezvousTracker['deviceInfo'].softwareVersion = 'notAVersion'; + expect(rendezvousTracker.doesHostSupportEcpRendezvousTracking).to.be.false; + + rendezvousTracker['deviceInfo'].softwareVersion = undefined; + expect(rendezvousTracker.doesHostSupportEcpRendezvousTracking).to.be.false; + + rendezvousTracker['deviceInfo'] = undefined; + expect(rendezvousTracker.doesHostSupportEcpRendezvousTracking).to.be.false; + }); + }); + + describe('on', () => { + it('supports unsubscribing', () => { + const spy = sinon.spy(); + const disconnect = rendezvousTracker.on('rendezvous', spy); + rendezvousTracker['emit']('rendezvous', {}); + rendezvousTracker['emit']('rendezvous', {}); + expect(spy.callCount).to.eql(2); + disconnect(); + expect(spy.callCount).to.eql(2); + //disconnect again to fix code coverage + delete rendezvousTracker['emitter']; + disconnect(); + }); + }); + + describe('getIsTelnetRendezvousTrackingEnabled', () => { + async function doTest(rawResponse: string, expectedValue: boolean) { + const stub = sinon.stub(SceneGraphDebugCommandController.prototype, 'logrendezvous').returns(Promise.resolve({ + result: { + rawResponse: 'on\n' + } + } as any)); + expect( + await rendezvousTracker.getIsTelnetRendezvousTrackingEnabled() + ).to.be.true; + stub.restore(); + } + + it('handles various responses', async () => { + await doTest('on', true); + await doTest('on\n', true); + await doTest('on \n', true); + await doTest('off', false); + await doTest('off\n', false); + await doTest('off \n', false); + }); + + it('does not crash on missing response', async () => { + await doTest(undefined, true); + }); + + it('logs an error', async () => { + const stub = sinon.stub(rendezvousTracker['logger'], 'warn'); + sinon.stub( + SceneGraphDebugCommandController.prototype, 'logrendezvous' + ).returns( + Promise.reject(new Error('crash')) + ); + await rendezvousTracker.getIsTelnetRendezvousTrackingEnabled(); + expect(stub.called).to.be.true; + }); + }); + + describe('startEcpPingTimer', () => { + it('only sets the timer once', () => { + rendezvousTracker.startEcpPingTimer(); + const ecpPingTimer = rendezvousTracker['ecpPingTimer']; + rendezvousTracker.startEcpPingTimer(); + //the timer reference shouldn't have changed + expect(ecpPingTimer).to.eql(rendezvousTracker['ecpPingTimer']); + //stop the timer + rendezvousTracker.stopEcpPingTimer(); + expect(rendezvousTracker['ecpPingTimer']).to.be.undefined; + //stopping while stopped is a noop + rendezvousTracker.stopEcpPingTimer(); + }); + }); + + describe('pingEcpRendezvous ', () => { + it('works', async () => { + sinon.stub(rendezvousTracker, 'getEcpRendezvous').returns(Promise.resolve({ 'trackingEnabled': true, 'items': [{ 'id': '1403', 'startTime': '97771301', 'endTime': '97771319', 'lineNumber': '11', 'file': 'pkg:/components/Tasks/GetSubReddit.brs' }, { 'id': '1404', 'startTime': '97771322', 'endTime': '97771322', 'lineNumber': '15', 'file': 'pkg:/components/Tasks/GetSubReddit.brs' }] })); + await rendezvousTracker.pingEcpRendezvous(); + expect(rendezvousTracker['rendezvousHistory']).to.eql({ 'hitCount': 2, 'occurrences': { 'pkg:/components/Tasks/GetSubReddit.brs': { 'occurrences': { '11': { 'clientLineNumber': 11, 'clientPath': '/components/Tasks/GetSubReddit.brs', 'hitCount': 1, 'totalTime': 0.018, 'type': 'lineInfo' }, '15': { 'clientLineNumber': 15, 'clientPath': '/components/Tasks/GetSubReddit.brs', 'hitCount': 1, 'totalTime': 0, 'type': 'lineInfo' } }, 'hitCount': 2, 'totalTime': 0.018, 'type': 'fileInfo', 'zeroCostHitCount': 1 } }, 'totalTime': 0.018, 'type': 'historyInfo', 'zeroCostHitCount': 1 }); + }); + }); + + describe('activateEcpTracking', () => { + beforeEach(() => { + sinon.stub(rendezvousTracker, 'pingEcpRendezvous').returns(Promise.resolve()); + sinon.stub(rendezvousTracker, 'startEcpPingTimer').callsFake(() => { }); + sinon.stub(rendezvousTracker, 'toggleEcpRendezvousTracking').returns(Promise.resolve(true)); + }); + + it('does not activate if telnet and ecp are both off', async () => { + sinon.stub(rendezvousTracker as any, 'runSGLogrendezvousCommand').returns(Promise.resolve('')); + sinon.stub(rendezvousTracker, 'getIsEcpRendezvousTrackingEnabled').returns(Promise.resolve(false)); + sinon.stub(rendezvousTracker, 'getIsTelnetRendezvousTrackingEnabled').returns(Promise.resolve(false)); + expect( + await rendezvousTracker.activate() + ).to.be.false; + }); + + it('activates if telnet is enabled but ecp is disabled', async () => { + sinon.stub(rendezvousTracker as any, 'runSGLogrendezvousCommand').returns(Promise.resolve('')); + sinon.stub(rendezvousTracker, 'getIsEcpRendezvousTrackingEnabled').returns(Promise.resolve(false)); + sinon.stub(rendezvousTracker, 'getIsTelnetRendezvousTrackingEnabled').returns(Promise.resolve(true)); + expect( + await rendezvousTracker.activate() + ).to.be.true; + }); + + it('activates if telnet is disabled but ecp is enabled', async () => { + sinon.stub(rendezvousTracker as any, 'runSGLogrendezvousCommand').returns(Promise.resolve('')); + sinon.stub(rendezvousTracker, 'getIsEcpRendezvousTrackingEnabled').returns(Promise.resolve(true)); + sinon.stub(rendezvousTracker, 'getIsTelnetRendezvousTrackingEnabled').returns(Promise.resolve(false)); + expect( + await rendezvousTracker.activate() + ).to.be.true; + }); }); describe('processLog ', () => { it('filters out all rendezvous log lines', async () => { rendezvousTrackerMock.expects('emit').withArgs('rendezvous').once(); + rendezvousTracker['trackingSource'] = 'telnet'; + let expected = `channel: Start\nStarting data processing\nData processing completed\n`; assert.equal(await rendezvousTracker.processLog(logString), expected); assert.deepEqual(rendezvousTracker.getRendezvousHistory, expectedHistory); @@ -275,6 +432,8 @@ describe('BrightScriptFileUtils ', () => { it('does not filter out rendezvous log lines', async () => { rendezvousTrackerMock.expects('emit').withArgs('rendezvous').once(); rendezvousTracker.setConsoleOutput('full'); + rendezvousTracker['trackingSource'] = 'telnet'; + assert.equal(await rendezvousTracker.processLog(logString), logString); assert.deepEqual(rendezvousTracker.getRendezvousHistory, expectedHistory); rendezvousTrackerMock.verify(); @@ -286,12 +445,25 @@ describe('BrightScriptFileUtils ', () => { await rendezvousTracker.processLog(text) ).to.eql(text); }); + + it('does not crash for files not found by the source locator', async () => { + //return undefined for all sources requested + rendezvousTracker.registerSourceLocator(() => { + return undefined; + }); + expect( + (await rendezvousTracker.processLog(`10-16 01:42:27.126 [sg.node.BLOCK] Rendezvous[2442] at roku_ads_lib:/libsource/Roku_Ads_SG_Wrappers.brs(1262)\r\n` + )).trim() + ).to.eql(''); + //the test passes if it doesn't explode on the file path + }); }); describe('clearHistory', () => { it('to reset the history data', async () => { rendezvousTrackerMock.expects('emit').withArgs('rendezvous').twice(); let expected = `channel: Start\nStarting data processing\nData processing completed\n`; + rendezvousTracker['trackingSource'] = 'telnet'; assert.equal(await rendezvousTracker.processLog(logString), expected); assert.deepEqual(rendezvousTracker.getRendezvousHistory, expectedHistory); diff --git a/src/RendezvousTracker.ts b/src/RendezvousTracker.ts index 6633f9b2..e1dd210f 100644 --- a/src/RendezvousTracker.ts +++ b/src/RendezvousTracker.ts @@ -2,9 +2,19 @@ import { EventEmitter } from 'events'; import * as path from 'path'; import * as replaceLast from 'replace-last'; import type { SourceLocation } from './managers/LocationManager'; +import { logger } from './logging'; +import { SceneGraphDebugCommandController } from './SceneGraphDebugCommandController'; +import * as xml2js from 'xml2js'; +import { util } from './util'; +import * as semver from 'semver'; +import type { DeviceInfo } from 'roku-deploy'; +import type { LaunchConfiguration } from './LaunchConfiguration'; export class RendezvousTracker { - constructor() { + constructor( + private deviceInfo: DeviceInfo, + private launchConfiguration: LaunchConfiguration + ) { this.clientPathsMap = {}; this.emitter = new EventEmitter(); this.filterOutLogs = true; @@ -18,6 +28,24 @@ export class RendezvousTracker { private rendezvousBlocks: RendezvousBlocks; private rendezvousHistory: RendezvousHistory; + /** + * Where should the rendezvous data be tracked from? If ecp, then the ecp ping data will be reported. If telnet, then any + * rendezvous data from telnet will reported. If 'off', then no data will be reported + */ + private trackingSource: 'telnet' | 'ecp' | 'off' = 'off'; + + /** + * Determine if the current Roku device supports the ECP rendezvous tracking feature + */ + public get doesHostSupportEcpRendezvousTracking() { + let softwareVersion = this.deviceInfo?.softwareVersion; + if (!semver.valid(softwareVersion)) { + softwareVersion = '0.0.0'; + } + return semver.gte(softwareVersion, '11.5.0'); + } + + public logger = logger.createLogger(`[${RendezvousTracker.name}]`); public on(eventname: 'rendezvous', handler: (output: RendezvousHistory) => void); public on(eventName: string, handler: (payload: any) => void) { this.emitter.on(eventName, handler); @@ -60,10 +88,173 @@ export class RendezvousTracker { * Clears the current rendezvous history */ public clearHistory() { + this.logger.log('Clear rendezvous history'); this.rendezvousHistory = this.createNewRendezvousHistory(); this.emit('rendezvous', this.rendezvousHistory); } + private ecpPingTimer: NodeJS.Timer; + + public startEcpPingTimer(): void { + this.logger.log('Start ecp ping timer'); + if (!this.ecpPingTimer) { + this.ecpPingTimer = setInterval(() => { + void this.pingEcpRendezvous(); + }, 1000); + } + } + + public stopEcpPingTimer() { + if (this.ecpPingTimer) { + clearInterval(this.ecpPingTimer); + this.ecpPingTimer = undefined; + } + } + + public async pingEcpRendezvous(): Promise { + try { + // Get ECP rendezvous data, parse it, and send it to event emitter + let ecpData = await this.getEcpRendezvous(); + const items = ecpData?.items ?? []; + if (items.length > 0) { + for (let blockInfo of items) { + let duration = ((parseInt(blockInfo.endTime) - parseInt(blockInfo.startTime)) / 1000).toString(); + this.rendezvousBlocks[blockInfo.id] = { + fileName: await this.updateClientPathMap(blockInfo.file, parseInt(blockInfo.lineNumber)), + lineNumber: blockInfo.lineNumber + }; + this.parseRendezvousLog(this.rendezvousBlocks[blockInfo.id], duration); + } + this.emit('rendezvous', this.rendezvousHistory); + } + } catch (e) { + //if there was an error pinging rendezvous, log the error but don't bring down the app + console.error('There was an error fetching rendezvous data', e?.stack); + } + } + + /** + * Determine if rendezvous tracking is enabled via the 8080 telnet command + */ + public async getIsTelnetRendezvousTrackingEnabled() { + return (await this.runSGLogrendezvousCommand('status'))?.trim()?.toLowerCase() === 'on'; + } + + /** + * Run a SceneGraph logendezvous 8080 command and get the text output + */ + private async runSGLogrendezvousCommand(command: 'status' | 'on' | 'off'): Promise { + let sgDebugCommandController = new SceneGraphDebugCommandController(this.launchConfiguration.host, this.launchConfiguration.sceneGraphDebugCommandsPort); + try { + this.logger.info(`port 8080 command: logrendezvous ${command}`); + return (await sgDebugCommandController.logrendezvous(command)).result.rawResponse; + } catch (error) { + this.logger.warn(`An error occurred running SG command "${command}"`, error); + } finally { + await sgDebugCommandController.end(); + } + } + + /** + * Determine if rendezvous tracking is enabled via the ECP command + */ + public async getIsEcpRendezvousTrackingEnabled() { + let ecpData = await this.getEcpRendezvous(); + return ecpData.trackingEnabled; + } + + public async activate(): Promise { + //if ECP tracking is supported, turn that on + if (this.doesHostSupportEcpRendezvousTracking) { + this.logger.log('Activating rendezvous tracking'); + // Toggle ECP tracking off and on to clear the log and then continue tracking + let untrack = await this.toggleEcpRendezvousTracking('untrack'); + let track = await this.toggleEcpRendezvousTracking('track'); + const isEcpTrackingEnabled = untrack && track && await this.getIsEcpRendezvousTrackingEnabled(); + if (isEcpTrackingEnabled) { + this.logger.info('ECP tracking is enabled'); + this.trackingSource = 'ecp'; + this.startEcpPingTimer(); + + //disable telnet rendezvous tracking since ECP is working + try { + await this.runSGLogrendezvousCommand('off'); + } catch { } + return true; + } + } + + this.logger.log('ECP tracking is not supported or had an issue. Trying to use telnet rendezvous tracking'); + //ECP tracking is not supported (or had an issue). Try enabling telnet rendezvous tracking (that only works with run_as_process=0, but worth a try...) + await this.runSGLogrendezvousCommand('on'); + if (await this.getIsTelnetRendezvousTrackingEnabled()) { + this.logger.log('telnet rendezvous tracking is enabled'); + this.trackingSource = 'telnet'; + return true; + } else { + this.logger.log('telnet rendezvous tracking is disabled or encountered an issue. rendezvous tracking is now disabled'); + } + return false; + } + + /** + * Get the response from an ECP sgrendezvous request from the Roku + */ + public async getEcpRendezvous(): Promise { + const url = `http://${this.launchConfiguration.host}:${this.launchConfiguration.remotePort}/query/sgrendezvous`; + this.logger.info(`Sending ECP rendezvous request:`, url); + // Send rendezvous query to ECP + const rendezvousQuery = await util.httpGet(url); + let rendezvousQueryData = rendezvousQuery.body as string; + let ecpData: EcpRendezvousData = { + trackingEnabled: false, + items: [] + }; + + this.logger.debug('Parsing rendezvous response', rendezvousQuery); + // Parse rendezvous query data + await new Promise((resolve, reject) => { + xml2js.parseString(rendezvousQueryData, (err, result) => { + if (err) { + reject(err); + } else { + const itemArray = result.sgrendezvous.data[0].item; + ecpData.trackingEnabled = result.sgrendezvous.data[0]['tracking-enabled'][0]; + if (Array.isArray(itemArray)) { + ecpData.items = itemArray.map((obj: any) => ({ + id: obj.id[0], + startTime: obj['start-tm'][0], + endTime: obj['end-tm'][0], + lineNumber: obj['line-number'][0], + file: obj.file[0] + })); + } + resolve(ecpData); + } + }); + }); + this.logger.debug('Parsed ECP rendezvous data:', ecpData); + return ecpData; + } + + /** + * Enable/Disable ECP Rendezvous tracking on the Roku device + * @returns true if successful, false if there was an issue setting the value + */ + public async toggleEcpRendezvousTracking(toggle: 'track' | 'untrack'): Promise { + try { + this.logger.log(`Sending ecp sgrendezvous request: ${toggle}`); + const response = await util.httpPost( + `http://${this.launchConfiguration.host}:${this.launchConfiguration.remotePort}/sgrendezvous/${toggle}`, + //not sure if we need this, but it works...so probably better to just leave it here + { body: '' } + ); + return true; + } catch (e) { + return false; + } + } + /** * Takes the debug output from the device and parses it for any rendezvous information. * Also if consoleOutput was not set to 'full' then any rendezvous output will be filtered from the output. @@ -79,68 +270,30 @@ export class RendezvousTracker { let match = /\[sg\.node\.(BLOCK|UNBLOCK)\s{0,}\] Rendezvous\[(\d+)\](?:\s\w+\n|\s\w{2}\s(.*)\((\d+)\)|[\s\w]+(\d+\.\d+)+|\s\w+)/g.exec(line); // see the following for an explanation for this regex: https://regex101.com/r/In0t7d/6 if (match) { - let [, type, id, fileName, lineNumber, duration] = match; - if (type === 'BLOCK') { - // detected the start of a rendezvous event - this.rendezvousBlocks[id] = { - fileName: await this.updateClientPathMap(fileName, parseInt(lineNumber)), - lineNumber: lineNumber - }; - } else if (type === 'UNBLOCK' && this.rendezvousBlocks[id]) { - // detected the completion of a rendezvous event - dataChanged = true; - let blockInfo = this.rendezvousBlocks[id]; - let clientLineNumber: string = this.clientPathsMap[blockInfo.fileName].clientLines[blockInfo.lineNumber].toString(); - - if (this.rendezvousHistory.occurrences[blockInfo.fileName]) { - // file is in history - if (this.rendezvousHistory.occurrences[blockInfo.fileName].occurrences[clientLineNumber]) { - // line is in history, just update it - this.rendezvousHistory.occurrences[blockInfo.fileName].occurrences[clientLineNumber].totalTime += this.getTime(duration); - this.rendezvousHistory.occurrences[blockInfo.fileName].occurrences[clientLineNumber].hitCount++; - } else { - // new line to be added to a file in history - this.rendezvousHistory.occurrences[blockInfo.fileName].occurrences[clientLineNumber] = this.createLineObject(blockInfo.fileName, parseInt(clientLineNumber), duration); - } - } else { - // new file to be added to the history - this.rendezvousHistory.occurrences[blockInfo.fileName] = { - occurrences: { - [clientLineNumber]: this.createLineObject(blockInfo.fileName, parseInt(clientLineNumber), duration) - }, - hitCount: 0, - totalTime: 0, - type: 'fileInfo', - zeroCostHitCount: 0 + if (this.trackingSource === 'telnet') { + let [, type, id, fileName, lineNumber, duration] = match; + if (type === 'BLOCK') { + // detected the start of a rendezvous event + this.rendezvousBlocks[id] = { + fileName: await this.updateClientPathMap(fileName, parseInt(lineNumber)), + lineNumber: lineNumber }; + } else if (type === 'UNBLOCK' && this.rendezvousBlocks[id]) { + // detected the completion of a rendezvous event + dataChanged = true; + let blockInfo = this.rendezvousBlocks[id]; + this.parseRendezvousLog(blockInfo, duration); + + // remove this event from pre history tracking + delete this.rendezvousBlocks[id]; } - - // how much time to add to the files total time - let timeToAdd = this.getTime(duration); - - // increment hit count and add to the total time for this file - this.rendezvousHistory.occurrences[blockInfo.fileName].hitCount++; - this.rendezvousHistory.hitCount++; - - // increment hit count and add to the total time for the history as a whole - this.rendezvousHistory.occurrences[blockInfo.fileName].totalTime += timeToAdd; - this.rendezvousHistory.totalTime += timeToAdd; - - if (timeToAdd === 0) { - this.rendezvousHistory.occurrences[blockInfo.fileName].zeroCostHitCount++; - this.rendezvousHistory.zeroCostHitCount++; - } - - // remove this event from pre history tracking - delete this.rendezvousBlocks[id]; } - + // still need to empty logs even if rendezvous tracking through ECP is enabled if (this.filterOutLogs) { lines.splice(i--, 1); } } } - if (dataChanged) { this.emit('rendezvous', this.rendezvousHistory); } @@ -148,6 +301,48 @@ export class RendezvousTracker { return lines.join('\n'); } + private parseRendezvousLog(blockInfo: { fileName: string; lineNumber: string }, duration: string) { + let clientLineNumber: string = this.clientPathsMap[blockInfo.fileName]?.clientLines[blockInfo.lineNumber].toString() ?? blockInfo.lineNumber; + if (this.rendezvousHistory.occurrences[blockInfo.fileName]) { + // file is in history + if (this.rendezvousHistory.occurrences[blockInfo.fileName].occurrences[clientLineNumber]) { + // line is in history, just update it + this.rendezvousHistory.occurrences[blockInfo.fileName].occurrences[clientLineNumber].totalTime += this.getTime(duration); + this.rendezvousHistory.occurrences[blockInfo.fileName].occurrences[clientLineNumber].hitCount++; + } else { + // new line to be added to a file in history + this.rendezvousHistory.occurrences[blockInfo.fileName].occurrences[clientLineNumber] = this.createLineObject(blockInfo.fileName, parseInt(clientLineNumber), duration); + } + } else { + // new file to be added to the history + this.rendezvousHistory.occurrences[blockInfo.fileName] = { + occurrences: { + [clientLineNumber]: this.createLineObject(blockInfo.fileName, parseInt(clientLineNumber), duration) + }, + hitCount: 0, + totalTime: 0, + type: 'fileInfo', + zeroCostHitCount: 0 + }; + } + + // how much time to add to the files total time + let timeToAdd = this.getTime(duration); + + // increment hit count and add to the total time for this file + this.rendezvousHistory.occurrences[blockInfo.fileName].hitCount++; + this.rendezvousHistory.hitCount++; + + // increment hit count and add to the total time for the history as a whole + this.rendezvousHistory.occurrences[blockInfo.fileName].totalTime += timeToAdd; + this.rendezvousHistory.totalTime += timeToAdd; + + if (timeToAdd === 0) { + this.rendezvousHistory.occurrences[blockInfo.fileName].zeroCostHitCount++; + this.rendezvousHistory.zeroCostHitCount++; + } + } + /** * Checks the client path map for existing path data and adds new data to the map if not found * @param fileName The filename or path parsed from the rendezvous output @@ -189,13 +384,15 @@ export class RendezvousTracker { } } let sourceLocation = await this.getSourceLocation(fileName, lineNumber); - this.clientPathsMap[fileName] = { - clientPath: sourceLocation.filePath, - clientLines: { - //TODO - should the line be 1 or 0 based? - [lineNumber]: sourceLocation.lineNumber - } - }; + if (sourceLocation) { + this.clientPathsMap[fileName] = { + clientPath: sourceLocation.filePath, + clientLines: { + //TODO - should the line be 1 or 0 based? + [lineNumber]: sourceLocation.lineNumber + } + }; + } } else if (!this.clientPathsMap[fileName].clientLines[lineNumber]) { // Add new client line to clint path map this.clientPathsMap[fileName].clientLines[lineNumber] = (await this.getSourceLocation(fileName, lineNumber)).lineNumber; @@ -226,7 +423,7 @@ export class RendezvousTracker { private createLineObject(fileName: string, lineNumber: number, duration?: string): RendezvousLineInfo { return { clientLineNumber: lineNumber, - clientPath: this.clientPathsMap[fileName].clientPath, + clientPath: this.clientPathsMap[fileName]?.clientPath ?? fileName, hitCount: 1, totalTime: this.getTime(duration), type: 'lineInfo' @@ -240,6 +437,25 @@ export class RendezvousTracker { private getTime(duration?: string): number { return duration ? parseFloat(duration) : 0.000; } + + /** + * Destroy/tear down this class + */ + public async destroy() { + this.emitter?.removeAllListeners(); + this.stopEcpPingTimer(); + //turn off ECP rendezvous tracking + if (this.doesHostSupportEcpRendezvousTracking) { + await this.toggleEcpRendezvousTracking('untrack'); + } + + //turn off telnet rendezvous tracking + try { + await this.runSGLogrendezvousCommand('off'); + } catch (e) { + this.logger.error('Failed to disable logrendezvous over 8080', e); + } + } } export interface RendezvousHistory { @@ -271,6 +487,19 @@ type RendezvousBlocks = Record; +interface EcpRendezvousData { + trackingEnabled: boolean; + items: EcpRendezvousItem[]; +} + +interface EcpRendezvousItem { + id: string; + startTime: string; + endTime: string; + lineNumber: string; + file: string; +} + type ElementType = 'fileInfo' | 'historyInfo' | 'lineInfo'; type RendezvousClientPathMap = Record { + return this.exec(`brightscript_warnings ${warningLimit ?? 100}`); + } + + /** * Send any custom command to the SceneGraph debug server. * diff --git a/src/adapters/DebugProtocolAdapter.spec.ts b/src/adapters/DebugProtocolAdapter.spec.ts index ccce0c05..38ba6506 100644 --- a/src/adapters/DebugProtocolAdapter.spec.ts +++ b/src/adapters/DebugProtocolAdapter.spec.ts @@ -1,72 +1,562 @@ +/* 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; - beforeEach(() => { + let server: DebugProtocolServer; + let client: DebugProtocolClient; + let plugin: DebugProtocolServerTestPlugin; + let breakpointManager: BreakpointManager; + let projectManager: ProjectManager; + let launchConfig = { + host: '192.168.1.5', + remotePort: 8060 + }; + let deviceInfo = { + softwareVersion: '11.5.0' + }; + let rendezvousTracker = new RendezvousTracker(deviceInfo, launchConfig as any); - adapter = new DebugProtocolAdapter({ + 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({}, {} as any); + breakpointManager = new BreakpointManager(sourcemapManager, locationManager); + projectManager = new ProjectManager(breakpointManager, locationManager); + projectManager.mainProject = new Project({ + rootDir: rootDir, + files: [], + outDir: outDir }); - socketDebugger = new Debugger(undefined); - adapter['socketDebugger'] = socketDebugger; + 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(); }); - 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); + afterEach(async () => { + sinon.restore(); + await 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() { + sinon.stub(adapter, 'processTelnetOutput').callsFake(async () => {}); + + await adapter.connect(); + await adapter['createDebugProtocolClient'](); + + client = adapter['client']; + 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' + }] + }) + ); + //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('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--; }); - socketDebugger['stopped'] = true; + + 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 + }); + + //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 04c0e796..8bc0b39d 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -1,30 +1,41 @@ -import type { 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 } from '../RendezvousTracker'; -import { RendezvousTracker } from '../RendezvousTracker'; +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 } 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 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 'roku-deploy'; +import type { ThreadsResponse } from '../debugProtocol/events/responses/ThreadsResponse'; /** * A class that connects to a Roku device over telnet debugger port and provides a standardized way of interacting with it. */ export class DebugProtocolAdapter { constructor( - private options: AdapterOptions + private options: AdapterOptions & ConstructorOptions, + private projectManager: ProjectManager, + private breakpointManager: BreakpointManager, + private rendezvousTracker: RendezvousTracker, + private deviceInfo: DeviceInfo ) { util.normalizeAdapterOptions(this.options); this.emitter = new EventEmitter(); this.chanperfTracker = new ChanperfTracker(); - this.rendezvousTracker = new RendezvousTracker(); this.compileErrorProcessor = new CompileErrorProcessor(); this.connected = false; @@ -32,15 +43,13 @@ export class DebugProtocolAdapter { this.chanperfTracker.on('chanperf', (output) => { this.emit('chanperf', output); }); - - // watch for rendezvous events - this.rendezvousTracker.on('rendezvous', (output) => { - this.emit('rendezvous', output); - }); } - private logger = logger.createLogger(`[${DebugProtocolAdapter.name}]`); + private logger = logger.createLogger(`[padapter]`); + /** + * Indicates whether the adapter has successfully established a connection with the device + */ public connected: boolean; /** @@ -53,8 +62,7 @@ export class DebugProtocolAdapter { private compileErrorProcessor: CompileErrorProcessor; private emitter: EventEmitter; private chanperfTracker: ChanperfTracker; - private rendezvousTracker: RendezvousTracker; - private socketDebugger: Debugger; + private client: DebugProtocolClient; private nextFrameId = 1; private stackFramesCache: Record = {}; @@ -64,57 +72,67 @@ export class DebugProtocolAdapter { * Get the version of the protocol for the Roku device we're currently connected to. */ public get activeProtocolVersion() { - return this.socketDebugger?.protocolVersion; + return this.client?.protocolVersion; } - 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: 'app-ready'): 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: (event: BreakpointsVerifiedEvent) => void); public on(eventName: 'cannot-continue', handler: () => void); public on(eventname: 'chanperf', handler: (output: ChanperfData) => void); public on(eventName: 'close', handler: () => void); public on(eventName: 'app-exit', handler: () => void); - public on(eventName: 'compile-errors', handler: (params: { path: string; lineNumber: number }[]) => void); + public on(eventName: 'diagnostics', handler: (params: BSDebugDiagnostic[]) => void); public on(eventName: 'connected', handler: (params: boolean) => void); public on(eventname: 'console-output', handler: (output: string) => void); // TODO: might be able to remove this at some point. public on(eventname: 'protocol-version', handler: (output: ProtocolVersionDetails) => void); - public on(eventname: 'rendezvous', handler: (output: RendezvousHistory) => void); public on(eventName: 'runtime-error', handler: (error: BrightScriptRuntimeError) => void); public on(eventName: 'suspend', handler: () => void); public on(eventName: 'start', handler: () => void); + public on(eventName: 'waiting-for-debugger', 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( - /* eslint-disable */ - eventName: - 'app-exit' | - 'cannot-continue' | - 'chanperf' | - 'close' | - 'compile-errors' | - 'connected' | - 'console-output' | - 'protocol-version' | - 'rendezvous' | - 'runtime-error' | - 'start' | - 'suspend' | - 'unhandled-console-output', - /* eslint-enable */ - data? - ) { + private emit(eventName: 'suspend'); + private emit(eventName: 'breakpoints-verified', event: BreakpointsVerifiedEvent); + private emit(eventName: 'diagnostics', data: BSDebugDiagnostic[]); + private emit(eventName: 'app-exit' | 'app-ready' | 'cannot-continue' | 'chanperf' | 'close' | 'connected' | 'console-output' | 'protocol-version' | 'rendezvous' | 'runtime-error' | 'start' | 'unhandled-console-output' | 'waiting-for-debugger', 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 @@ -135,16 +153,16 @@ export class DebugProtocolAdapter { */ public isAppRunning = false; - public async activate() { + public activate() { this.isActivated = true; - await this.handleStartupIfReady(); + this.handleStartupIfReady(); } public async sendErrors() { await this.compileErrorProcessor.sendErrors(); } - private async handleStartupIfReady() { + private handleStartupIfReady() { if (this.isActivated && this.isAppRunning) { this.emit('start'); @@ -152,8 +170,7 @@ export class DebugProtocolAdapter { //If not, then there are probably still messages being received, so let the normal handler //emit the suspend event when it's ready if (this.isAtDebuggerPrompt === true) { - let threads = await this.getThreads(); - this.emit('suspend', threads[0].threadId); + this.emit('suspend'); } } } @@ -187,19 +204,35 @@ export class DebugProtocolAdapter { } public get isAtDebuggerPrompt() { - return this.socketDebugger ? this.socketDebugger.isStopped : false; + return this.client?.isStopped ?? false; } /** * Connect to the telnet session. This should be called before the channel is launched. */ public async connect() { + //Start processing telnet output to look for compile errors or the debugger prompt + await this.processTelnetOutput(); + + this.on('waiting-for-debugger', () => { + void this.createDebugProtocolClient(); + }); + } + + public async createDebugProtocolClient() { let deferred = defer(); - this.socketDebugger = new Debugger(this.options); + if (this.client) { + await Promise.race([ + util.sleep(2000), + await this.client.destroy() + ]); + this.client = undefined; + } + this.client = new DebugProtocolClient(this.options); try { // Emit IO from the debugger. // eslint-disable-next-line @typescript-eslint/no-misused-promises - this.socketDebugger.on('io-output', async (responseText) => { + this.client.on('io-output', async (responseText) => { if (typeof responseText === 'string') { responseText = this.chanperfTracker.processLog(responseText); responseText = await this.rendezvousTracker.processLog(responseText); @@ -209,7 +242,7 @@ export class DebugProtocolAdapter { }); // Emit IO from the debugger. - this.socketDebugger.on('protocol-version', (data: ProtocolVersionDetails) => { + this.client.on('protocol-version', (data: ProtocolVersionDetails) => { if (data.errorCode === PROTOCOL_ERROR_CODES.SUPPORTED) { this.emit('console-output', data.message); } else if (data.errorCode === PROTOCOL_ERROR_CODES.NOT_TESTED) { @@ -223,29 +256,33 @@ export class DebugProtocolAdapter { // TODO: Update once we know the exact version of the debug protocol this issue was fixed in. // Due to casing issues with variables on protocol version and under we first need to try the request in the supplied case. // If that fails we retry in lower case. - this.enableVariablesLowerCaseRetry = semver.satisfies(this.activeProtocolVersion, '*'); + this.enableVariablesLowerCaseRetry = semver.satisfies(this.activeProtocolVersion, '<3.1.0'); // While execute was added as a command in 2.1.0. It has shortcoming that prevented us for leveraging the command. // This was mostly addressed in the 3.0.0 release to the point where we were comfortable adding support for the command. this.supportsExecuteCommand = semver.satisfies(this.activeProtocolVersion, '>=3.0.0'); }); // Listen for the close event - this.socketDebugger.on('close', () => { + this.client.on('close', () => { this.emit('close'); this.beginAppExit(); + void this.client?.destroy(); + this.client = undefined; }); // Listen for the app exit event - this.socketDebugger.on('app-exit', () => { + this.client.on('app-exit', () => { this.emit('app-exit'); + void this.client?.destroy(); + this.client = undefined; }); - this.socketDebugger.on('suspend', (data) => { + this.client.on('suspend', (data) => { this.clearCache(); - this.emit('suspend', data); + this.emit('suspend'); }); - this.socketDebugger.on('runtime-error', (data) => { + this.client.on('runtime-error', (data) => { console.debug('hasRuntimeError!!', data); this.emit('runtime-error', { message: data.data.stopReasonDetail, @@ -253,21 +290,47 @@ export class DebugProtocolAdapter { }); }); - this.socketDebugger.on('cannot-continue', () => { + this.client.on('cannot-continue', () => { this.emit('cannot-continue'); }); - this.connected = await this.socketDebugger.connect(); + //handle when the device verifies breakpoints + this.client.on('breakpoints-verified', (event) => { + let unverifiableDeviceIds = [] as number[]; - this.logger.log(`Closing telnet connection used for compile errors`); - if (this.compileClient) { - this.compileClient.removeAllListeners(); - this.compileClient.destroy(); - this.compileClient = undefined; - } + //mark the breakpoints as verified + for (let breakpoint of event?.breakpoints ?? []) { + 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.client.removeBreakpoints(unverifiableDeviceIds); + } + this.emit('breakpoints-verified', event); + }); + + this.client.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); + }); + + await this.client.connect(); this.logger.log(`Connected to device`, { host: this.options.host, connected: this.connected }); + this.connected = true; this.emit('connected', this.connected); + this.emit('app-ready'); //the adapter is connected and running smoothly. resolve the promise deferred.resolve(); @@ -284,13 +347,26 @@ export class DebugProtocolAdapter { }, 200); } - public async watchCompileOutput() { + /** + * Determines if the current version of the debug protocol supports emitting compile error updates. + */ + public get supportsCompileErrorReporting() { + return semver.satisfies(this.deviceInfo.brightscriptDebuggerVersion, '>=3.1.0'); + } + + private processingTelnetOutput = false; + public async processTelnetOutput() { + if (this.processingTelnetOutput) { + return; + } + this.processingTelnetOutput = true; + let deferred = defer(); try { this.compileClient = new Socket(); - this.compileErrorProcessor.on('compile-errors', (errors) => { + this.compileErrorProcessor.on('diagnostics', (errors) => { this.compileClient.end(); - this.emit('compile-errors', errors); + this.emit('diagnostics', errors); }); //if the connection fails, reject the connect promise @@ -299,7 +375,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'); @@ -322,6 +398,7 @@ export class DebugProtocolAdapter { lastPartialLine = ''; } // Emit the completed io string. + this.findWaitForDebuggerPrompt(responseText.trim()); this.compileErrorProcessor.processUnhandledLines(responseText.trim()); this.emit('unhandled-console-output', responseText.trim()); } @@ -335,22 +412,31 @@ export class DebugProtocolAdapter { return deferred.promise; } + private findWaitForDebuggerPrompt(responseText: string) { + let lines = responseText.split(/\r?\n/g); + for (const line of lines) { + if (/Waiting for debugger on \d+\.\d+\.\d+\.\d+:8081/g.exec(line)) { + this.emit('waiting-for-debugger'); + } + } + } + /** * Send command to step over */ public async stepOver(threadId: number) { this.clearCache(); - return this.socketDebugger.stepOver(threadId); + return this.client.stepOver(threadId); } public async stepInto(threadId: number) { this.clearCache(); - return this.socketDebugger.stepIn(threadId); + return this.client.stepIn(threadId); } public async stepOut(threadId: number) { this.clearCache(); - return this.socketDebugger.stepOut(threadId); + return this.client.stepOut(threadId); } /** @@ -358,7 +444,7 @@ export class DebugProtocolAdapter { */ public async continue() { this.clearCache(); - return this.socketDebugger.continue(); + return this.client.continue(); } /** @@ -367,7 +453,7 @@ export class DebugProtocolAdapter { public async pause() { this.clearCache(); //send the kill signal, which breaks into debugger mode - return this.socketDebugger.pause(); + return this.client.pause(); } /** @@ -383,7 +469,7 @@ export class DebugProtocolAdapter { * @param command * @returns the output of the command (if possible) */ - public async evaluate(command: string, frameId: number = this.socketDebugger.primaryThread): Promise { + public async evaluate(command: string, frameId: number = this.client.primaryThread): Promise { if (this.supportsExecuteCommand) { if (!this.isAtDebuggerPrompt) { throw new Error('Cannot run evaluate: debugger is not paused'); @@ -395,16 +481,21 @@ export class DebugProtocolAdapter { } this.logger.log('evaluate ', { command, frameId }); - const response = await this.socketDebugger.executeCommand(command, stackFrame.frameIndex, stackFrame.threadIndex); + const response = await this.client.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' }; } @@ -416,29 +507,34 @@ export class DebugProtocolAdapter { } } - public async getStackTrace(threadId: number = this.socketDebugger.primaryThread) { + public async getStackTrace(threadIndex: number = this.client.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.client.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++, - frameIndex: stackTraceData.stackSize - i - 1, // frame index is the reverse of the returned order. - threadIndex: threadId, - // eslint-disable-next-line no-nested-ternary - filePath: i === 0 ? (frameData.fileName) ? frameData.fileName : thread.filePath : frameData.fileName, - lineNumber: i === 0 ? thread.lineNumber : frameData.lineNumber, + // frame index is the reverse of the returned order. + 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) }; this.stackFramesCache[stackFrame.frameId] = stackFrame; frames.push(stackFrame); } + //if the first frame is missing any data, suppliment with thread information + if (frames[0]) { + frames[0].filePath ??= thread.filePath; + frames[0].lineNumber ??= thread.lineNumber; + } return frames; }); @@ -453,11 +549,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'); @@ -468,7 +565,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 @@ -476,84 +573,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.client.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.client.getVariables(variablePath, frame.frameIndex, frame.threadIndex); } + return response; + } - 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 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 (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'; - } + /** + * 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; + } + } - let container = { - name: expression, - evaluateName: expression, - variablePath: variablePath, - type: variableType, - value: value, - keyType: variable.keyType, - elementCount: variable.elementCount - }; + /** + * 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; + } - 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 - }; - } + 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; + } - 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); - } + //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 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; } /** @@ -571,7 +709,7 @@ export class DebugProtocolAdapter { } /** - * Get a list of threads. The first thread in the list is the active thread + * Get a list of threads. The active thread will always be first in the list. */ public async getThreads() { if (!this.isAtDebuggerPrompt) { @@ -579,18 +717,35 @@ export class DebugProtocolAdapter { } return this.resolve('threads', async () => { let threads: Thread[] = []; - let threadsData = await this.socketDebugger.threads(); + let threadsResponse: ThreadsResponse; + // sometimes roku threads are stubborn and haven't stopped yet, causing our ThreadsRequest to fail with "not stopped". + // A nice simple fix for this is to just send a "pause" request again, which seems to fix the issue. + // we'll do this a few times just to make sure we've tried our best to get the list of threads. + for (let i = 0; i < 3; i++) { + threadsResponse = await this.client.threads(); + if (threadsResponse.data.errorCode === ErrorCode.NOT_STOPPED) { + this.logger.log(`Threads request retrying... ${i}:\n`, threadsResponse); + threadsResponse = undefined; + const pauseResponse = await this.client.pause(true); + await util.sleep(100); + } else { + break; + } + } + if (!threadsResponse) { + return []; + } - 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: this.client.primaryThread === i, // isSelected: threadInfo.isPrimary, - filePath: threadInfo.fileName, + filePath: threadInfo.filePath, functionName: threadInfo.functionName, - lineNumber: threadInfo.lineNumber + 1, //protocol is 0-based but 1-based is expected + lineNumber: threadInfo.lineNumber, //threadInfo.lineNumber is 1-based. Thread requires 1-based line numbers lineContents: threadInfo.codeSnippet, threadId: i }; @@ -615,31 +770,40 @@ 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() { - if (this.socketDebugger) { - // destroy might be called due to a compile error so the socket debugger might not exist yet - await this.socketDebugger.exitChannel(); + this.isDestroyed = true; + + // destroy the debug client if it's defined + if (this.client) { + try { + await this.client.destroy(); + } catch (e) { + this.logger.error(e); + } } this.cache = undefined; - if (this.emitter) { - this.emitter.removeAllListeners(); - } + this.removeAllListeners(); this.emitter = undefined; - } - // #region Rendezvous Tracker pass though functions - /** - * Passes the debug functions used to locate the client files and lines to the RendezvousTracker - */ - public registerSourceLocator(sourceLocator: (debuggerPath: string, lineNumber: number) => Promise) { - this.rendezvousTracker.registerSourceLocator(sourceLocator); + if (this.compileClient) { + this.compileClient.removeAllListeners(); + this.compileClient.destroy(); + this.compileClient = undefined; + } } /** @@ -664,7 +828,106 @@ export class DebugProtocolAdapter { public clearChanperfHistory() { this.chanperfTracker.clearHistory(); } - // #endregion + + 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.client?.supportsBreakpointRegistrationWhileRunning && !this.isAtDebuggerPrompt) { + this.logger.info('Cannot sync breakpoints because the debugger', this.client.supportsBreakpointRegistrationWhileRunning ? 'does not support sending breakpoints while running' : 'is not paused'); + return; + } + + //compute breakpoint changes since last sync + const diff = await this.breakpointManager.getDiff(this.projectManager.getAllProjects()); + this.logger.log('Syncing breakpoints', diff); + + if (diff.added.length === 0 && diff.removed.length === 0) { + this.logger.debug('No breakpoints to sync'); + return; + } + + // REMOVE breakpoints (delete these breakpoints from the device) + if (diff.removed.length > 0) { + const response = await this.client.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) { + const breakpointsToSendToDevice = diff.added.map(breakpoint => { + const hitCount = parseInt(breakpoint.hitCondition); + return { + filePath: breakpoint.pkgPath, + lineNumber: breakpoint.line, + hitCount: !isNaN(hitCount) ? hitCount : undefined, + conditionalExpression: breakpoint.condition, + srcHash: breakpoint.srcHash, + destHash: breakpoint.destHash, + componentLibraryName: breakpoint.componentLibraryName + }; + }); + + //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); + } + } + for (const breakpoints of [standardBreakpoints, conditionalBreakpoints]) { + const response = await this.client.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].srcHash, + breakpoints[i].destHash, + deviceBreakpoint.id + ); + } + + //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) + ); + } + } + } + } } export interface StackFrame { @@ -683,10 +946,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[]; @@ -701,6 +963,9 @@ export enum KeyType { export interface Thread { isSelected: boolean; + /** + * The 1-based line number for the thread + */ lineNumber: number; filePath: string; functionName: string; @@ -712,3 +977,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.spec.ts b/src/adapters/TelnetAdapter.spec.ts index 2264e6a6..78069485 100644 --- a/src/adapters/TelnetAdapter.spec.ts +++ b/src/adapters/TelnetAdapter.spec.ts @@ -3,14 +3,27 @@ import type { EvaluateContainer } from './TelnetAdapter'; import { TelnetAdapter } from './TelnetAdapter'; import * as dedent from 'dedent'; import { HighLevelType } from '../interfaces'; +import { RendezvousTracker } from '../RendezvousTracker'; +import type { LaunchConfiguration } from '../LaunchConfiguration'; describe('TelnetAdapter ', () => { let adapter: TelnetAdapter; + let launchConfig = { + 'host': '192.168.1.5', + 'remotePort': 8060 + }; + let deviceInfo = { + softwareVersion: '11.5.0' + }; + let rendezvousTracker = new RendezvousTracker(deviceInfo, launchConfig as any); beforeEach(() => { - adapter = new TelnetAdapter({ - host: '127.0.0.1' - }); + adapter = new TelnetAdapter( + { + host: '127.0.0.1' + }, + rendezvousTracker + ); }); describe('getHighLevelTypeDetails', () => { @@ -28,9 +41,9 @@ describe('TelnetAdapter ', () => { vscode_is_string:falsetrue vscode_is_string:falsefalse vscode_is_string:truecat - vscode_is_string:truecat + vscode_is_string:truecat + vscode_is_string:true vscode_is_string:true - vscode_is_string:true `).length).to.equal(6); }); it('handles basic arrays', () => { diff --git a/src/adapters/TelnetAdapter.ts b/src/adapters/TelnetAdapter.ts index 72b15521..b740b67a 100644 --- a/src/adapters/TelnetAdapter.ts +++ b/src/adapters/TelnetAdapter.ts @@ -1,11 +1,11 @@ import { orderBy } from 'natural-orderby'; import * as EventEmitter from 'eventemitter3'; import { Socket } from 'net'; -import * as rokuDeploy from 'roku-deploy'; +import { rokuDeploy } from 'roku-deploy'; import { PrintedObjectParser } from '../PrintedObjectParser'; +import type { BSDebugDiagnostic } from '../CompileErrorProcessor'; import { CompileErrorProcessor } from '../CompileErrorProcessor'; -import type { RendezvousHistory } from '../RendezvousTracker'; -import { RendezvousTracker } from '../RendezvousTracker'; +import type { RendezvousTracker } from '../RendezvousTracker'; import type { ChanperfData } from '../ChanperfTracker'; import { ChanperfTracker } from '../ChanperfTracker'; import type { SourceLocation } from '../managers/LocationManager'; @@ -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. @@ -22,7 +23,8 @@ export class TelnetAdapter { constructor( private options: AdapterOptions & { enableDebuggerAutoRecovery?: boolean; - } + }, + private rendezvousTracker: RendezvousTracker ) { util.normalizeAdapterOptions(this.options); this.options.enableDebuggerAutoRecovery ??= false; @@ -32,22 +34,24 @@ export class TelnetAdapter { this.debugStartRegex = /BrightScript Micro Debugger\./ig; this.debugEndRegex = /Brightscript Debugger>/ig; this.chanperfTracker = new ChanperfTracker(); - this.rendezvousTracker = new RendezvousTracker(); this.compileErrorProcessor = new CompileErrorProcessor(); - // watch for chanperf events this.chanperfTracker.on('chanperf', (output) => { this.emit('chanperf', output); }); + } - // watch for rendezvous events - this.rendezvousTracker.on('rendezvous', (output) => { - this.emit('rendezvous', output); - }); + private connectionDeferred = defer(); + + public isConnected(): Promise { + return this.connectionDeferred.promise; } - public logger = logger.createLogger(`[${TelnetAdapter.name}]`); + public logger = logger.createLogger(`[tadapter]`); + /** + * Indicates whether the adapter has successfully established a connection with the device + */ public connected: boolean; private compileErrorProcessor: CompileErrorProcessor; @@ -58,17 +62,24 @@ export class TelnetAdapter { private debugStartRegex: RegExp; private debugEndRegex: RegExp; private chanperfTracker: ChanperfTracker; - private rendezvousTracker: RendezvousTracker; private cache = {}; - public readonly supportsMultipleRuns = true; - /** * Does this adapter support the `execute` command (known as `eval` in telnet) */ public supportsExecute = true; + public once(eventName: 'app-ready'): 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 @@ -78,10 +89,9 @@ export class TelnetAdapter { public on(eventname: 'chanperf', handler: (output: ChanperfData) => void); public on(eventName: 'close', handler: () => void); public on(eventName: 'app-exit', handler: () => void); - public on(eventName: 'compile-errors', handler: (params: { path: string; lineNumber: number }[]) => void); + public on(eventName: 'diagnostics', handler: (params: BSDebugDiagnostic[]) => void); public on(eventName: 'connected', handler: (params: boolean) => void); public on(eventname: 'console-output', handler: (output: string) => void); - public on(eventname: 'rendezvous', handler: (output: RendezvousHistory) => void); public on(eventName: 'runtime-error', handler: (error: BrightScriptRuntimeError) => void); public on(eventName: 'suspend', handler: () => void); public on(eventName: 'start', handler: () => void); @@ -95,14 +105,15 @@ export class TelnetAdapter { }; } + private emit(eventName: 'diagnostics', data: BSDebugDiagnostic[]); private emit( /* eslint-disable @typescript-eslint/indent */ eventName: 'app-exit' | + 'app-ready' | 'cannot-continue' | 'chanperf' | 'close' | - 'compile-errors' | 'connected' | 'console-output' | 'rendezvous' | @@ -111,8 +122,8 @@ export class TelnetAdapter { 'suspend' | 'unhandled-console-output', /* eslint-enable @typescript-eslint/indent */ - data? - ) { + 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 @@ -243,6 +254,7 @@ export class TelnetAdapter { client.connect(this.options.brightScriptConsolePort, this.options.host, () => { this.logger.log(`Telnet connection established to ${this.options.host}:${this.options.brightScriptConsolePort}`); this.connected = true; + this.connectionDeferred.resolve(); this.emit('connected', this.connected); }); @@ -261,8 +273,8 @@ export class TelnetAdapter { }); //listen for any compile errors - this.compileErrorProcessor.on('compile-errors', (errors) => { - this.emit('compile-errors', errors); + this.compileErrorProcessor.on('diagnostics', (errors) => { + this.emit('diagnostics', errors); }); //listen for any console output that was not handled by other methods in the adapter @@ -294,9 +306,13 @@ export class TelnetAdapter { return; } + //emitting this signal so the BrightScriptDebugSession will successfully complete it's publish method. + if (/\[beacon.signal\] \|AppCompileComplete/i.exec(responseText.trim())) { + this.emit('app-ready'); + } + if (this.isActivated) { //watch for the start of the program - // eslint-disable-next-line no-cond-assign if (/\[scrpt.ctx.run.enter\]/i.exec(responseText.trim())) { this.isAppRunning = true; this.logger.log('Running beacon detected', { responseText }); @@ -304,7 +320,6 @@ export class TelnetAdapter { } //watch for the end of the program - // eslint-disable-next-line no-cond-assign if (/\[beacon.report\] \|AppExitComplete/i.exec(responseText.trim())) { this.beginAppExit(); } @@ -471,8 +486,7 @@ export class TelnetAdapter { let regexp = /#(\d+)\s+(?:function|sub)\s+([\$\w\d]+).*\s+file\/line:\s+(.*)\((\d+)\)/ig; let matches: RegExpExecArray; let frames: StackFrame[] = []; - // eslint-disable-next-line no-cond-assign - while (matches = regexp.exec(responseText)) { + while ((matches = regexp.exec(responseText))) { //the first index is the whole string //then the matches should be in pairs for (let i = 1; i < matches.length; i += 4) { @@ -518,10 +532,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'); } @@ -875,7 +888,6 @@ export class TelnetAdapter { type: '', highLevelType: HighLevelType.uninitialized, evaluateName: undefined, - variablePath: [], elementCount: -1, value: '', keyType: KeyType.legacy, @@ -1016,10 +1028,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(); } @@ -1034,14 +1053,6 @@ export class TelnetAdapter { return Promise.resolve(); } - // #region Rendezvous Tracker pass though functions - /** - * Passes the debug functions used to locate the client files and lines to the RendezvousTracker - */ - public registerSourceLocator(sourceLocator: (debuggerPath: string, lineNumber: number) => Promise) { - this.rendezvousTracker.registerSourceLocator(sourceLocator); - } - /** * Passes the log level down to the RendezvousTracker and ChanperfTracker * @param outputLevel the consoleOutput from the launch config @@ -1064,7 +1075,10 @@ export class TelnetAdapter { public clearChanperfHistory() { this.chanperfTracker.clearHistory(); } - // #endregion + + public async syncBreakpoints() { + //we can't send dynamic breakpoints to the server...so just do nothing + } } export interface StackFrame { @@ -1081,7 +1095,6 @@ export enum EventName { export interface EvaluateContainer { name: string; evaluateName: string; - variablePath: string[]; type: string; value: string; keyType: KeyType; @@ -1098,10 +1111,25 @@ export enum KeyType { } export interface Thread { + /** + * Is this thread selected + */ isSelected: boolean; + /** + * The 1-based line number + */ lineNumber: number; + /** + * The pkgPath to the file on-device + */ filePath: string; + /** + * The contents of the line (i.e. the code for the line) + */ lineContents: string; + /** + * The id of this thread + */ threadId: number; } @@ -1117,3 +1145,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/adapters/TelnetRequestPipeline.spec.ts b/src/adapters/TelnetRequestPipeline.spec.ts index b7e091de..01514a3a 100644 --- a/src/adapters/TelnetRequestPipeline.spec.ts +++ b/src/adapters/TelnetRequestPipeline.spec.ts @@ -1,10 +1,12 @@ -import { TelnetRequestPipeline } from './TelnetRequestPipeline'; -import { util } from '../util'; +import { TelnetCommand, TelnetRequestPipeline } from './TelnetRequestPipeline'; +import { defer, util } from '../util'; import { expect } from 'chai'; import dedent = require('dedent'); import { logger } from '../logging'; import { clean } from '../testHelpers.spec'; import { Deferred } from 'brighterscript'; +import { createSandbox } from 'sinon'; +const sinon = createSandbox(); const prompt = 'Brightscript Debugger>'; @@ -46,6 +48,7 @@ describe('RequestPipeline', () => { }; beforeEach(() => { + sinon.restore(); logger.logLevel = 'off'; consoleOutput = ''; unhandledConsoleOutput = ''; @@ -61,6 +64,10 @@ describe('RequestPipeline', () => { }); }); + afterEach(() => { + sinon.restore(); + }); + it('handles split debugger prompt messages', async () => { handleData(prompt); socket.messageQueue.push([ @@ -178,82 +185,138 @@ describe('RequestPipeline', () => { `); }); - describe('', () => { - it('correctly handles both types of line endings', async () => { - //send prompt so pipeline will execute commands - handleData(prompt); - socket.messageQueue.push([ - 'vscode_key_start:message1:vscode_key_stop vscode_is_string:trueHello\r\n' + - 'vscode_key_start:message2:vscode_key_stop vscode_is_string:trueWorld\r\n' + - '\r\n' + - 'Brightscript Debugger>' - ]); - expect( - await pipeline.executeCommand('commandDoesNotMatter', { waitForPrompt: true }) - ).to.equal( - 'vscode_key_start:message1:vscode_key_stop vscode_is_string:trueHello\r\n' + - 'vscode_key_start:message2:vscode_key_stop vscode_is_string:trueWorld\r\n\r\n' - ); + it('correctly handles both types of line endings', async () => { + //send prompt so pipeline will execute commands + handleData(prompt); + socket.messageQueue.push([ + 'vscode_key_start:message1:vscode_key_stop vscode_is_string:trueHello\r\n' + + 'vscode_key_start:message2:vscode_key_stop vscode_is_string:trueWorld\r\n' + + '\r\n' + + 'Brightscript Debugger>' + ]); + expect( + await pipeline.executeCommand('commandDoesNotMatter', { waitForPrompt: true }) + ).to.equal( + 'vscode_key_start:message1:vscode_key_stop vscode_is_string:trueHello\r\n' + + 'vscode_key_start:message2:vscode_key_stop vscode_is_string:trueWorld\r\n\r\n' + ); + }); + + it('removes warning statements introduced in 10.5', async () => { + //send prompt so pipeline will execute commands + handleData(prompt); + socket.messageQueue.push([ + 'Warning: operation may not be interruptible.\r\n' + + 'Invalid' + + '\r\n' + + 'Brightscript Debugger>' + ]); + expect( + await pipeline.executeCommand('commandDoesNotMatter', { waitForPrompt: true }) + ).to.equal( + 'Invalid\r\n' + ); + }); + + it('Removes "thread attached" messages', async () => { + //send prompt so pipeline will execute commands + handleData(prompt); + socket.messageQueue.push([ + 'Warning: operation may not be interruptible.', + 'roAssociativeArray', + '', + 'Brightscript Debugger> ', + '', + 'Thread attached: pkg:/source/main.brs(6) screen.show()', + '', + '', + '' + ].join('\r\n')); + expect( + await pipeline.executeCommand('commandDoesNotMatter', { waitForPrompt: true }) + ).to.equal( + 'roAssociativeArray\r\n\r\n' + ); + }); + + it('joins split log messages together', async () => { + socket.messageQueue.push(); + const outputEvents = [] as string[]; + const deferred = defer(); + //there should be 2 events + pipeline.on('console-output', (data) => { + outputEvents.push(data); + if (outputEvents.length === 2) { + deferred.resolve(); + } }); + handleData('1'); + handleData('2\r\n'); + handleData('3'); + handleData('4\r\n'); + await deferred.promise; + expect(outputEvents).to.eql([ + '12\r\n', + '34\r\n' + ]); + }); - it('removes warning statements introduced in 10.5', async () => { - //send prompt so pipeline will execute commands - handleData(prompt); - socket.messageQueue.push([ - 'Warning: operation may not be interruptible.\r\n' + - 'Invalid' + - '\r\n' + - 'Brightscript Debugger>' - ]); - expect( - await pipeline.executeCommand('commandDoesNotMatter', { waitForPrompt: true }) - ).to.equal( - 'Invalid\r\n' - ); + it('moves on to the next command if the current command failed', async () => { + pipeline.isAtDebuggerPrompt = true; + let command1: TelnetCommand; + let command2: TelnetCommand; + + const executeStub = sinon.stub(TelnetCommand.prototype, 'execute').callsFake(function(this: TelnetCommand) { + // resolve command2 immediately + if (this === command2) { + command2['deferred'].resolve(''); + } }); - it('Removes "thread attached" messages', async () => { - //send prompt so pipeline will execute commands - handleData(prompt); - socket.messageQueue.push([ - 'Warning: operation may not be interruptible.', - 'roAssociativeArray', - '', - 'Brightscript Debugger> ', - '', - 'Thread attached: pkg:/source/main.brs(6) screen.show()', - '', - '', - '' - ].join('\r\n')); - expect( - await pipeline.executeCommand('commandDoesNotMatter', { waitForPrompt: true }) - ).to.equal( - 'roAssociativeArray\r\n\r\n' - ); + void pipeline.executeCommand('test 1', { waitForPrompt: true }); + command1 = pipeline['activeCommand']; + void pipeline.executeCommand('test 2', { waitForPrompt: true }); + command2 = pipeline['commands'][0]; + + //stub the logger function so it throws an exception + const loggerDebugStub = sinon.stub(command1.logger, 'debug').callsFake(() => { + //only crash the first time + if (loggerDebugStub.callCount === 1) { + throw new Error('Crash!'); + } }); - it('joins split log messages together', async () => { - socket.messageQueue.push(); - const outputEvents = [] as string[]; - const deferred = new Deferred(); - //there should be 2 events - pipeline.on('console-output', (data) => { - outputEvents.push(data); - if (outputEvents.length === 2) { - deferred.resolve(); - } + //pass some bad data to the command, which causes it to throw an exception + pipeline['handleData'](`bad data\n/${prompt}`); + + //make sure this test actually did what we thought...that the logger.debug() function was called and had a chance to throw + expect(loggerDebugStub.called).to.be.true; + //restore the logger function so the next command doesn't crash + loggerDebugStub.restore(); + + //command1 should be a rejected promise + expect(command1['deferred'].isRejected).to.be.true; + + //wait for command2 to finish executing + await command2.promise; + expect(command2['deferred'].isResolved).to.be.true; + + //should have executed the second command after the first one failed + expect(executeStub.callCount).to.equal(2); + + }); + + describe('TelnetCommand', () => { + it('serializes to just the important bits', () => { + const command = new TelnetCommand('print m', true, logger, pipeline, 3); + expect( + JSON.parse(JSON.stringify(command)) + ).to.eql({ + id: 3, + commandText: 'print m', + waitForPrompt: true, + isCompleted: false }); - handleData('1'); - handleData('2\r\n'); - handleData('3'); - handleData('4\r\n'); - await deferred.promise; - expect(outputEvents).to.eql([ - '12\r\n', - '34\r\n' - ]); }); }); - }); diff --git a/src/adapters/TelnetRequestPipeline.ts b/src/adapters/TelnetRequestPipeline.ts index ad4ca962..3d15250b 100644 --- a/src/adapters/TelnetRequestPipeline.ts +++ b/src/adapters/TelnetRequestPipeline.ts @@ -1,6 +1,6 @@ import type { Socket } from 'net'; import * as EventEmitter from 'eventemitter3'; -import { util } from '../util'; +import { defer, util } from '../util'; import type { Logger } from '../logging'; import { createLogger } from '../logging'; import { Deferred } from 'brighterscript'; @@ -14,7 +14,7 @@ export class TelnetRequestPipeline { private logger = createLogger(`[${TelnetRequestPipeline.name}]`); - private commands: Command[] = []; + private commands: TelnetCommand[] = []; public isAtDebuggerPrompt = false; @@ -22,7 +22,7 @@ export class TelnetRequestPipeline { return this.activeCommand !== undefined; } - private activeCommand: Command = undefined; + private activeCommand: TelnetCommand = undefined; private emitter = new EventEmitter(); @@ -138,7 +138,7 @@ export class TelnetRequestPipeline { */ insertAtFront?: boolean; }) { - const command = new Command( + const command = new TelnetCommand( commandText, options?.waitForPrompt ?? true, this.logger, @@ -212,7 +212,7 @@ export class TelnetRequestPipeline { } } -class Command { +export class TelnetCommand { public constructor( public commandText: string, /** @@ -228,7 +228,7 @@ class Command { public logger: Logger; - private deferred = new Deferred(); + private deferred = defer(); /** * Promise that completes when the command is finished @@ -257,7 +257,7 @@ class Command { this.pipeline.isAtDebuggerPrompt = false; } catch (e) { this.logger.error('Error executing command', e); - this.deferred.reject('Error executing command'); + this.deferred.reject(new Error('Error executing command')); } } @@ -279,30 +279,43 @@ class Command { //get the first response const match = /Brightscript Debugger>\s*/is.exec(pipeline.unhandledText); if (match) { - const response = this.removeJunk( - pipeline.unhandledText.substring(0, match.index) - ); - - this.logger.debug('Found response before the first "Brightscript Debugger>" prompt', { response, allText: pipeline.unhandledText }); - //remove the response from the unhandled text - pipeline.unhandledText = pipeline.unhandledText.substring(match.index + match[0].length); - - //emit the remaining unhandled text - if (pipeline.unhandledText?.length > 0) { - pipeline.emit('unhandled-console-output', pipeline.unhandledText); - } - //clear the unhandled text - pipeline.unhandledText = ''; - - this.logger.debug(`execute result`, { commandText: this.commandText, response }); - if (!this.deferred.isCompleted) { - this.logger.debug('resolving promise', { response }); - this.deferred.resolve(response); - } else { - this.logger.error('Command already completed', { response, commandText: this.commandText, stacktrace: new Error().stack }); + try { + const response = this.removeJunk( + pipeline.unhandledText.substring(0, match.index) + ); + + this.logger.debug('Found response before the first "Brightscript Debugger>" prompt', { response, allText: pipeline.unhandledText }); + //remove the response from the unhandled text + pipeline.unhandledText = pipeline.unhandledText.substring(match.index + match[0].length); + + //emit the remaining unhandled text + if (pipeline.unhandledText?.length > 0) { + pipeline.emit('unhandled-console-output', pipeline.unhandledText); + } + //clear the unhandled text + pipeline.unhandledText = ''; + + this.logger.debug(`execute result`, { commandText: this.commandText, response }); + if (!this.deferred.isCompleted) { + this.logger.debug('resolving promise', { response }); + this.deferred.resolve(response); + } else { + this.logger.error('Command already completed', { response, commandText: this.commandText, stacktrace: new Error().stack }); + } + } catch (e) { + this.deferred.reject(e as unknown as Error); } } else { // no prompt found, wait for more data from the device } } + + toJSON() { + return { + commandText: this.commandText, + id: this.id, + isCompleted: this.isCompleted, + waitForPrompt: this.waitForPrompt + }; + } } diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 00000000..7aa33ba3 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,17 @@ +#!/usr/bin/env node +import * as yargs from 'yargs'; +import { BrightScriptDebugSession } from './debugSession/BrightScriptDebugSession'; + +let options = yargs + .usage('$0', 'BrighterScript, a superset of Roku\'s BrightScript language') + .help('help', 'View help information about this tool.') + .option('dap', { type: 'boolean', defaultDescription: 'false', description: 'Run roku-debug as a standalone debug-adapter-protocol process, communicating over STDIO' }) + .parseSync(); + +(function main() { + if (options.dap) { + BrightScriptDebugSession.run(BrightScriptDebugSession); + } else { + throw new Error('Not supported'); + } +}()); diff --git a/src/debugProtocol/Constants.ts b/src/debugProtocol/Constants.ts index e016e7c4..dacc6ca8 100644 --- a/src/debugProtocol/Constants.ts +++ b/src/debugProtocol/Constants.ts @@ -4,29 +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 - 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, @@ -35,63 +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, - ALL_THREADS_STOPPED = 2, - THREAD_ATTACHED = 3 +export enum StopReason { + /** + * Uninitialized stopReason. + */ + Undefined = 'Undefined', + /** + * Thread is running. + */ + NotStopped = 'NotStopped', + /** + * 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. + */ + RuntimeError = 'RuntimeError' } - -export enum VARIABLE_FLAGS { - isChildKey = 0x01, - isConst = 0x02, - isContainer = 0x04, - isNameHere = 0x08, - isRefCounted = 0x10, - isValueHere = 0x20 +/** + * 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_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 +/** + * Human-readable UpdateType values. To get the codes, use the `UpdateTypeCode` enum + */ +export enum UpdateType { + Undefined = 'Undefined', + /** + * 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. + */ + IOPortOpened = 'IOPortOpened', + /** + * 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. + */ + AllThreadsStopped = 'AllThreadsStopped', + /** + * 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 + */ + ThreadAttached = 'ThreadAttached', + /** + * A compilation or runtime error occurred when evaluating the cond_expr of a conditional breakpoint + * @since protocol 3.1 + */ + BreakpointError = 'BreakpointError', + /** + * A compilation error occurred + * @since protocol 3.1 + */ + CompileError = 'CompileError', + /** + * Breakpoints were successfully verified + * @since protocol 3.2 + */ + BreakpointVerified = 'BreakpointVerified' } -//#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; - } +/** + * 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 } + 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..54d8c6ea --- /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(() => { + void 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(() => { + void 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 6f2fc61e..00000000 --- a/src/debugProtocol/Debugger.spec.ts +++ /dev/null @@ -1,150 +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 } 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); - }); - }); -}); diff --git a/src/debugProtocol/Debugger.ts b/src/debugProtocol/Debugger.ts deleted file mode 100644 index 578a0e93..00000000 --- a/src/debugProtocol/Debugger.ts +++ /dev/null @@ -1,624 +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 } from './Constants'; -import { SmartBuffer } from 'smart-buffer'; -import { logger } from '../logging'; -import { ERROR_CODES, UPDATE_TYPES } from '..'; -import { ExecuteResponseV3 } from './responses/ExecuteResponseV3'; -import { ListBreakpointsResponse } from './responses/ListBreakpointsResponse'; -import { AddBreakpointsResponse } from './responses/AddBreakpointsResponse'; -import { RemoveBreakpointsResponse } from './responses/RemoveBreakpointsResponse'; - -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, - stopOnEntry: false, - //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 firstRunContinueFired = false; - private stopped = false; - private totalRequests = 0; - private activeRequests = {}; - private options: ConstructorOptions; - - /** - * Get a promise that resolves after an event occurs exactly once - */ - 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: 'data' | 'runtime-error' | 'suspend', handler: (data: any) => 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( - /* eslint-disable */ - eventName: - 'app-exit' | - 'cannot-continue' | - 'close' | - 'connected' | - 'data' | - 'handshake-verified' | - 'io-output' | - 'protocol-version' | - 'runtime-error' | - 'start' | - 'suspend', - /* eslint-disable */ - 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 - if (this.emitter) { - this.emitter.emit(eventName, data); - } - }, 0); - } - - public async connect(): Promise { - this.logger.log('connect', this.options); - const debugSetupEnd = 'total socket debugger setup time'; - console.time(debugSetupEnd); - - // Create a new TCP client.` - this.controllerClient = new Net.Socket(); - // Send a connection request to the server. - - this.controllerClient.connect({ port: this.options.controllerPort, host: this.options.host }, () => { - // If there is no error, the server has accepted the request and created a new - // socket dedicated to us. - this.logger.log('TCP connection established with the server.'); - - // The client can also receive data from the server by reading from its socket. - // The client can now send data to the server by writing to its socket. - let buffer = new SmartBuffer({ size: Buffer.byteLength(Debugger.DEBUGGER_MAGIC) + 1 }).writeStringNT(Debugger.DEBUGGER_MAGIC).toBuffer(); - this.logger.log('Sending magic to server'); - this.controllerClient.write(buffer); - }); - - this.controllerClient.on('data', (buffer) => { - if (this.unhandledData) { - this.unhandledData = Buffer.concat([this.unhandledData, buffer]); - } else { - this.unhandledData = buffer; - } - - this.parseUnhandledData(this.unhandledData); - }); - - 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) => { - console.error(`TCP connection error`, error); - this.shutdown('close'); - }); - - let connectPromise: Promise = new Promise((resolve, reject) => { - let disconnect = this.on('connected', (connected) => { - disconnect(); - console.timeEnd(debugSetupEnd); - if (connected) { - resolve(connected); - } else { - reject(connected); - } - }); - }); - - return connectPromise; - } - - public async continue() { - let result; - if (this.stopped) { - this.stopped = false; - result = this.makeRequest(new SmartBuffer({ size: 12 }), COMMANDS.CONTINUE); - } - return result; - } - - 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) { - 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); - } - } - - public async getVariables(variablePathEntries: Array = [], getChildKeys = true, stackFrameIndex: number = this.stackFrameIndex, threadIndex: number = this.primaryThread) { - if (this.stopped && threadIndex > -1) { - let buffer = new SmartBuffer({ size: 17 }); - buffer.writeUInt8(getChildKeys ? 1 : 0); // variable_request_flags - buffer.writeUInt32LE(threadIndex); // thread_index - buffer.writeUInt32LE(stackFrameIndex); // stack_frame_index - buffer.writeUInt32LE(variablePathEntries.length); // variable_path_len - variablePathEntries.forEach(variablePathEntry => { - buffer.writeStringNT(variablePathEntry); // variable_path_entries - optional - }); - 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: BreakpointSpec[]): Promise { - if (breakpoints?.length > 0) { - let buffer = new SmartBuffer(); - buffer.writeUInt32LE(breakpoints.length); // num_breakpoints - The number of breakpoints in the breakpoints array. - breakpoints.forEach((breakpoint) => { - buffer.writeStringNT(breakpoint.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. - }); - return this.makeRequest(buffer, COMMANDS.ADD_BREAKPOINTS); - } - return new AddBreakpointsResponse(null); - } - - 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, - extraData: extraData - }; - - return new Promise((resolve, reject) => { - let unsubscribe = this.on('data', (data) => { - if (data.requestId === requestId) { - unsubscribe(); - resolve(data); - } - }); - - if (this.controllerClient) { - this.controllerClient.write(buffer.toBuffer()); - } 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 data - ', `bytes: ${buffer.length}`, debuggerRequestResponse) - 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); - 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); - 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: - 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); - if (!debuggerHandshake.success) { - debuggerHandshake = new HandshakeResponse(buffer); - } - - if (debuggerHandshake.success) { - this.handshakeComplete = true; - this.verifyHandshake(debuggerHandshake); - this.removedProcessedBytes(debuggerHandshake, buffer); - 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) { - 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]', (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('not tested'); - 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) { - 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) => { - 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.log(`Error: ${err}`); - }); - - this.emit('connected', true); - }); - - this.removedProcessedBytes(connectIoPortResponse, unhandledData, packetLength); - return true; - } - return false - } - - private async 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 (!this.firstRunContinueFired && !this.options.stopOnEntry) { - this.logger.log('Sending first run continue command'); - await this.continue(); - this.firstRunContinueFired = true; - } else 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; -} - - -export interface ConstructorOptions { - /** - * The host/ip address of the Roku - */ - host: string; - /** - * If true, the application being debugged will stop on the first line of the program. - */ - stopOnEntry?: boolean; - /** - * 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; -} 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..16d21aac --- /dev/null +++ b/src/debugProtocol/client/DebugProtocolClient.ts @@ -0,0 +1,1321 @@ +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(`[dpclient]`); + + // The highest tested version of the protocol we support. + public supportedVersionRange = '<=3.2.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 async emit(eventName: string, data?) { + //emit these events on next tick, otherwise they will be processed immediately which could cause issues + await util.sleep(0); + //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); + } + + /** + * 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 async establishControlConnection() { + const connection = await new Promise((resolve) => { + const socket = new Net.Socket({ + allowHalfOpen: false + }); + socket.on('error', (error) => { + console.debug(Date.now(), 'Encountered an error connecting to the debug protocol socket.', error); + }); + socket.connect({ port: this.options.controlPort, host: this.options.host }, () => { + resolve(socket); + }); + }); + 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(); + //destroy the control socket since it just closed on us... + this.controlSocket?.destroy?.(); + this.controlSocket = undefined; + this.emit('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); + //destroy the control socket since it errored + this.controlSocket?.destroy?.(); + this.controlSocket = undefined; + this.emit('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(force = false) { + return this.processStopRequest( + StopRequest.fromJson({ + requestId: this.requestIdSequence++ + }), + force + ); + } + + private async processStopRequest(request: StopRequest, force = false) { + if (this.isStopped === false || force) { + 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; + } + } + //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 may 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.emit('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.emit('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); + this.emit('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(immediate); + } + + private shutdownPromise: Promise; + private async shutdown(immediate = false) { + if (this.shutdownPromise === undefined) { + this.logger.log('[shutdown] shutting down'); + this.shutdownPromise = this._shutdown(immediate); + } else { + this.logger.log(`[shutdown] Tried to call .shutdown() again. Returning the same promise`); + } + return this.shutdownPromise; + } + + private async _shutdown(immediate = false) { + 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 (only if the device is still listening...) + if (this.controlSocket) { + 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.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/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 ee98307f..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 = ERROR_CODES[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: string; -} - -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.ts b/src/debugProtocol/responses/VariableResponse.ts deleted file mode 100644 index 80f77ae2..00000000 --- a/src/debugProtocol/responses/VariableResponse.ts +++ /dev/null @@ -1,158 +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 'AA': - case 'Array': - 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 d60df11d..00000000 --- a/src/debugProtocol/responses/responseCreationHelpers.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { SmartBuffer } from 'smart-buffer'; -import type { ERROR_CODES, UPDATE_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); -} - -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); -} - -/** - * 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..1fc11897 --- /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 1453e920..d2da91ab 100644 --- a/src/debugSession/BrightScriptDebugSession.spec.ts +++ b/src/debugSession/BrightScriptDebugSession.spec.ts @@ -1,39 +1,45 @@ import { expect } from 'chai'; import * as assert from 'assert'; import * as fsExtra from 'fs-extra'; -import { standardizePath as s } from 'brighterscript'; import * as path from 'path'; import * as sinonActual from 'sinon'; import type { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; import { DebugSession } from 'vscode-debugadapter'; import { BrightScriptDebugSession } from './BrightScriptDebugSession'; import { fileUtils } from '../FileUtils'; -import type { EvaluateContainer } from '../adapters/TelnetAdapter'; -import { PrimativeType } from '../adapters/TelnetAdapter'; -import { defer } from '../util'; +import type { EvaluateContainer, StackFrame } from '../adapters/TelnetAdapter'; +import { PrimativeType, TelnetAdapter } from '../adapters/TelnetAdapter'; +import { defer, util } from '../util'; import { HighLevelType } from '../interfaces'; import type { LaunchConfiguration } from '../LaunchConfiguration'; import type { SinonStub } from 'sinon'; +import { DiagnosticSeverity, util as bscUtil, standardizePath as s } from 'brighterscript'; +import { DefaultFiles, rokuDeploy } from 'roku-deploy'; +import type { AddProjectParams, ComponentLibraryConstructorParams } from '../managers/ProjectManager'; +import { ComponentLibraryProject, Project } from '../managers/ProjectManager'; +import { RendezvousTracker } from '../RendezvousTracker'; +import { ClientToServerCustomEventName, isCustomRequestEvent } from './Events'; +import { EventEmitter } from 'eventemitter3'; const sinon = sinonActual.createSandbox(); -const tempDir = s`${__dirname}/../.tmp`; -const cwd = fileUtils.standardizePath(process.cwd()); -const outDir = fileUtils.standardizePath(`${cwd}/outDir`); -const stagingFolderPath = fileUtils.standardizePath(`${outDir}/stagingDir`); +const tempDir = s`${__dirname}/../../.tmp`; const rootDir = s`${tempDir}/rootDir`; +const outDir = s`${tempDir}/outDir`; +const stagingDir = s`${outDir}/stagingDir`; +const complib1Dir = s`${tempDir}/complib1`; describe('BrightScriptDebugSession', () => { let responseDeferreds = []; let responses = []; beforeEach(() => { - fsExtra.emptydirSync(rootDir); - fsExtra.emptydirSync(stagingFolderPath); - fsExtra.emptydirSync(outDir); + fsExtra.emptyDirSync(rootDir); + fsExtra.emptyDirSync(stagingDir); + fsExtra.emptyDirSync(outDir); }); afterEach(() => { - fsExtra.emptydirSync(tempDir); + fsExtra.emptyDirSync(tempDir); sinon.restore(); }); @@ -42,31 +48,38 @@ describe('BrightScriptDebugSession', () => { let launchConfiguration: LaunchConfiguration; let initRequestArgs: DebugProtocol.InitializeRequestArguments; - let rokuAdapter: any = { - on: () => { - return () => { - }; - }, - activate: () => Promise.resolve(), - registerSourceLocator: (a, b) => { }, - setConsoleOutput: (a) => { } - }; + let rokuAdapter: TelnetAdapter; + let errorSpy: sinon.SinonSpy; + beforeEach(() => { + fsExtra.emptydirSync(tempDir); sinon.restore(); + //prevent calling DebugSession.shutdown() because that calls process.kill(), which would kill the test session + sinon.stub(DebugSession.prototype, 'shutdown').returns(null); + try { session = new BrightScriptDebugSession(); } catch (e) { console.log(e); } + errorSpy = sinon.spy(session.logger, 'error'); //override the error response function and throw an exception so we can fail any tests (session as any).sendErrorResponse = (...args: string[]) => { throw new Error(args[2]); }; - launchConfiguration = {} as any; + launchConfiguration = { + rootDir: rootDir, + outDir: outDir, + stagingDir: stagingDir, + files: DefaultFiles + } as any; session['launchConfiguration'] = launchConfiguration; + session.projectManager.launchConfiguration = launchConfiguration; + session.breakpointManager.launchConfiguration = launchConfiguration; initRequestArgs = {} as any; session['initRequestArgs'] = initRequestArgs; + //mock the rokuDeploy module with promises so we can have predictable tests session.rokuDeploy = { prepublishToStaging: () => { @@ -93,19 +106,25 @@ describe('BrightScriptDebugSession', () => { } }; rokuAdapter = { - on: () => { - return () => { - }; - }, + emitter: new EventEmitter(), + on: TelnetAdapter.prototype.on, + once: TelnetAdapter.prototype.once, + emit: TelnetAdapter.prototype['emit'], activate: () => Promise.resolve(), registerSourceLocator: (a, b) => { }, setConsoleOutput: (a) => { }, evaluate: () => { }, - getVariable: () => { } - }; - (session as any).rokuAdapter = rokuAdapter; + syncBreakpoints: () => { }, + getVariable: () => { }, + getScopeVariables: (a) => { }, + getThreads: () => { + return []; + }, + getStackTrace: () => { } + } as any; + session['rokuAdapter'] = rokuAdapter; //mock the roku adapter - (session as any).connectRokuAdapter = () => { + session['connectRokuAdapter'] = () => { return Promise.resolve(rokuAdapter); }; @@ -130,6 +149,299 @@ describe('BrightScriptDebugSession', () => { }); }); + afterEach(() => { + fsExtra.emptydirSync(tempDir); + fsExtra.removeSync(outDir); + sinon.restore(); + }); + + it('supports external zipping process', async () => { + //write some project files + fsExtra.outputFileSync(`${rootDir}/source/main.brs`, ` + sub main() + print "hello" + end sub + `); + fsExtra.outputFileSync(`${rootDir}/manifest`, ''); + + const packagePath = s`${tempDir}/custom/app.zip`; + + //init the session + session.initializeRequest({} as any, {} as any); + + //set a breakpoint in main + await session.setBreakPointsRequest({} as any, { + source: { + path: s`${rootDir}/source/main.brs` + }, + breakpoints: [{ + line: 2 + }] + }); + + sinon.stub(rokuDeploy, 'getDeviceInfo').returns(Promise.resolve({ + developerEnabled: true + })); + sinon.stub(util, 'dnsLookup').callsFake((host) => Promise.resolve(host)); + + let sendEvent = session.sendEvent.bind(session); + sinon.stub(session, 'sendEvent').callsFake((event) => { + if (isCustomRequestEvent(event)) { + void rokuDeploy.zipFolder(session['launchConfiguration'].stagingDir, packagePath).then(() => { + //pretend we are the client and send a response back + session.emit(ClientToServerCustomEventName.customRequestEventResponse, { + requestId: event.body.requestId + }); + }); + } else { + //call through + return sendEvent(event); + } + }); + sinon.stub(session as any, 'connectRokuAdapter').callsFake(() => { + sinon.stub(session['rokuAdapter'], 'connect').returns(Promise.resolve()); + session['rokuAdapter'].connected = true; + return Promise.resolve(session['rokuAdapter']); + }); + + const publishStub = sinon.stub(session.rokuDeploy, 'publish').callsFake(() => { + //emit the app-ready event + (session['rokuAdapter'] as TelnetAdapter)['emit']('app-ready'); + + return Promise.resolve({ + message: 'success', + results: [] + }); + }); + + await session.launchRequest({} as any, { + cwd: tempDir, + //where the source files reside + rootDir: rootDir, + //where roku-debug should put the staged files (and inject breakpoints) + stagingDir: `${stagingDir}/staging`, + //the name of the task that should be run to create the zip (doesn't matter for this test...we're going to intercept it anyway) + packageTask: 'custom-build', + //where the packageTask will be placing the compiled zip + packagePath: packagePath, + packageUploadOverrides: { + route: '1234', + formData: { + one: 'two', + three: null + } + } + } as Partial as LaunchConfiguration); + + expect(publishStub.getCall(0).args[0].packageUploadOverrides).to.eql({ + route: '1234', + formData: { + one: 'two', + three: null + } + }); + }); + + describe('evaluateRequest', () => { + it('resets local var counter on suspend', async () => { + session['rokuAdapterDeferred'].resolve(session['rokuAdapter']); + + const stub = sinon.stub(session['rokuAdapter'], 'evaluate').callsFake(x => { + return Promise.resolve({ type: 'message', message: '' }); + }); + sinon.stub(rokuAdapter, 'getVariable').callsFake(x => { + return Promise.resolve({ + evaluateName: x, + highLevelType: 'primative', + value: '1' + } as EvaluateContainer); + }); + rokuAdapter.isAtDebuggerPrompt = true; + await session.evaluateRequest( + {} as DebugProtocol.EvaluateResponse, + { context: 'repl', expression: '1+2', frameId: 1 } as DebugProtocol.EvaluateArguments + ); + await session.evaluateRequest( + {} as DebugProtocol.EvaluateResponse, + { context: 'repl', expression: '2+3', frameId: 1 } as DebugProtocol.EvaluateArguments + ); + expect(stub.getCall(0).firstArg).to.eql(`${session.tempVarPrefix}eval = []`); + expect(stub.getCall(1).firstArg).to.eql(`${session.tempVarPrefix}eval[0] = 1+2`); + expect(stub.getCall(2).firstArg).to.eql(`${session.tempVarPrefix}eval[1] = 2+3`); + await session['onSuspend'](); + await session.evaluateRequest( + {} as DebugProtocol.EvaluateResponse, + { context: 'repl', expression: '3+4', frameId: 1 } as DebugProtocol.EvaluateArguments + ); + expect(stub.getCall(3).firstArg).to.eql(`${session.tempVarPrefix}eval = []`); + expect(stub.getCall(4).firstArg).to.eql(`${session.tempVarPrefix}eval[0] = 3+4`); + }); + + it('can assign to a variable', async () => { + session['rokuAdapterDeferred'].resolve(session['rokuAdapter']); + + const stub = sinon.stub(session['rokuAdapter'], 'evaluate').callsFake(x => { + return Promise.resolve({ type: 'message', message: '' }); + }); + sinon.stub(rokuAdapter, 'getVariable').callsFake(x => { + return Promise.resolve({ + evaluateName: x, + highLevelType: 'primative', + value: '1' + } as EvaluateContainer); + }); + rokuAdapter.isAtDebuggerPrompt = true; + await session.evaluateRequest( + {} as DebugProtocol.EvaluateResponse, + { context: 'repl', expression: 'testVar = "foo"', frameId: 1 } as DebugProtocol.EvaluateArguments + ); + expect(stub.getCall(0).firstArg).to.eql('testVar = "foo"'); + }); + + it('handels evaluating expressions on different threads', async () => { + session['rokuAdapterDeferred'].resolve(session['rokuAdapter']); + + const stub = sinon.stub(session['rokuAdapter'], 'evaluate').callsFake(x => { + return Promise.resolve({ type: 'message', message: '' }); + }); + sinon.stub(rokuAdapter, 'getVariable').callsFake(x => { + return Promise.resolve({ + evaluateName: x, + highLevelType: 'primative', + value: '1' + } as EvaluateContainer); + }); + rokuAdapter.isAtDebuggerPrompt = true; + await session.evaluateRequest( + {} as DebugProtocol.EvaluateResponse, + { context: 'repl', expression: '1+2', frameId: 1 } as DebugProtocol.EvaluateArguments + ); + expect(stub.getCall(0).firstArg).to.eql(`${session.tempVarPrefix}eval = []`); + expect(stub.getCall(1).firstArg).to.eql(`${session.tempVarPrefix}eval[0] = 1+2`); + await session.evaluateRequest( + {} as DebugProtocol.EvaluateResponse, + { context: 'repl', expression: '2+3', frameId: 2 } as DebugProtocol.EvaluateArguments + ); + expect(stub.getCall(2).firstArg).to.eql(`${session.tempVarPrefix}eval = []`); + expect(stub.getCall(3).firstArg).to.eql(`${session.tempVarPrefix}eval[0] = 2+3`); + }); + }); + + describe('variablesRequest', () => { + it('hides debug local variables', async () => { + session['rokuAdapterDeferred'].resolve(session['rokuAdapter']); + + sinon.stub(session['rokuAdapter'], 'evaluate').callsFake(x => { + return Promise.resolve({ type: 'message', message: '' }); + }); + sinon.stub(rokuAdapter, 'getScopeVariables').callsFake(() => { + return Promise.resolve(['m', 'top', `${session.tempVarPrefix}eval`]); + }); + sinon.stub(rokuAdapter, 'getVariable').callsFake(x => { + return Promise.resolve( + { + name: x, + highLevelType: 'primative', + value: '1' + } as EvaluateContainer); + }); + + let response: DebugProtocol.VariablesResponse = { + body: { + variables: [] + }, + request_seq: 0, + success: false, + command: '', + seq: 0, + type: '' + }; + + rokuAdapter.isAtDebuggerPrompt = true; + session['launchConfiguration'].enableVariablesPanel = true; + session['dispatchRequest']({ command: 'scopes', arguments: { frameId: 0 }, type: 'request', seq: 8 }); + await session.variablesRequest( + response, + { variablesReference: 1000, filter: 'named', start: 0, count: 0, format: '' } as DebugProtocol.VariablesArguments + ); + + expect( + response.body.variables.find(x => x.name.startsWith(session.tempVarPrefix)) + ).to.not.exist; + + session['launchConfiguration'].showHiddenVariables = true; + await session.variablesRequest( + response, + { variablesReference: 1000, filter: 'named', start: 0, count: 0, format: '' } as DebugProtocol.VariablesArguments + ); + expect( + response.body.variables.find(x => x.name.startsWith(session.tempVarPrefix)) + ).to.exist; + }); + + it('hides debug children variables', async () => { + session['rokuAdapterDeferred'].resolve(session['rokuAdapter']); + + sinon.stub(session['rokuAdapter'], 'evaluate').callsFake(x => { + return Promise.resolve({ type: 'message', message: '' }); + }); + + rokuAdapter.isAtDebuggerPrompt = true; + + let response: DebugProtocol.VariablesResponse = { + body: { + variables: [] + }, + request_seq: 0, + success: false, + command: '', + seq: 0, + type: '' + }; + //Set the this.variables + session['variables'][1001] = { + name: 'm', + value: 'roAssociativeArray', + variablesReference: 1, + childVariables: [ + { + name: '__rokudebug__eval', + value: 'true', + variablesReference: 0 + }, + { + name: 'top', + value: 'roSGNode:GetSubReddit', + variablesReference: 3 + }, + { + name: '[[count]]', + value: '3', + variablesReference: 0 + } + ] + }; + + await session.variablesRequest( + response, + { variablesReference: 1001, filter: 'named', start: 0, count: 0, format: '' } as DebugProtocol.VariablesArguments + ); + + expect( + response.body.variables.find(x => x.name.startsWith(session.tempVarPrefix)) + ).to.not.exist; + + session['launchConfiguration'].showHiddenVariables = true; + await session.variablesRequest( + response, + { variablesReference: 1001, filter: 'named', start: 0, count: 0, format: '' } as DebugProtocol.VariablesArguments + ); + expect( + response.body.variables.find(x => x.name.startsWith(session.tempVarPrefix)) + ).to.exist; + }); + }); + describe('initializeRequest', () => { it('does not throw', () => { assert.doesNotThrow(() => { @@ -171,6 +483,8 @@ describe('BrightScriptDebugSession', () => { } it('returns the correct boolean variable', async () => { + session['rokuAdapterDeferred'].resolve(session['rokuAdapter']); + let expression = 'someBool'; getVariableValue = getBooleanEvaluateContainer(expression); //adapter has to be at prompt for evaluates to work @@ -188,6 +502,8 @@ describe('BrightScriptDebugSession', () => { //this fails on TravisCI for some reason. TODO - fix this it('returns the correct indexed variables count', async () => { + session['rokuAdapterDeferred'].resolve(session['rokuAdapter']); + let expression = 'someArray'; getVariableValue = { name: expression, @@ -212,6 +528,8 @@ describe('BrightScriptDebugSession', () => { }); it('returns the correct named variables count', async () => { + session['rokuAdapterDeferred'].resolve(session['rokuAdapter']); + let expression = 'someObject'; getVariableValue = { name: expression, @@ -349,9 +667,36 @@ describe('BrightScriptDebugSession', () => { }); }); + describe('initRendezvousTracking', () => { + it('clears history when disabled', async () => { + const stub = sinon.stub(session, 'sendEvent'); + const activateStub = sinon.stub(RendezvousTracker.prototype, 'activate'); + const clearHistoryStub = sinon.stub(RendezvousTracker.prototype, 'clearHistory'); + + session['launchConfiguration'].rendezvousTracking = false; + + await session['initRendezvousTracking'](); + expect(clearHistoryStub.called).to.be.true; + expect(activateStub.called).to.be.false; + }); + + it('activates when not disabled', async () => { + const stub = sinon.stub(session, 'sendEvent'); + const activateStub = sinon.stub(RendezvousTracker.prototype, 'activate'); + const clearHistoryStub = sinon.stub(RendezvousTracker.prototype, 'clearHistory'); + + session['launchConfiguration'].rendezvousTracking = undefined; + + await session['initRendezvousTracking'](); + expect(clearHistoryStub.called).to.be.true; + expect(activateStub.called).to.be.true; + + }); + }); + describe('setBreakPointsRequest', () => { let response; - let args; + let args: DebugProtocol.SetBreakpointsArguments; beforeEach(() => { response = undefined; //intercept the sent response @@ -361,31 +706,35 @@ describe('BrightScriptDebugSession', () => { args = { source: { - path: path.normalize(`${rootDir}/dest/some/file.brs`) + path: s`${rootDir}/dest/some/file.brs` }, breakpoints: [] }; }); - it('returns correct results', () => { + it('returns correct results', async () => { + args.source.path = s`${rootDir}/source/main.brs`; + + fsExtra.outputFileSync(s`${rootDir}/manifest`, ''); + fsExtra.outputFileSync(s`${rootDir}/source/main.brs`, 'sub main()\nend sub'); args.breakpoints = [{ line: 1 }]; - session.setBreakPointsRequest({}, args); + await session.setBreakPointsRequest({}, args); expect(response.body.breakpoints[0]).to.deep.include({ line: 1, - verified: true + verified: false }); - //mark debugger as 'launched' which should change the behavior of breakpoints. - session.breakpointManager.lockBreakpoints(); + //simulate "launch" + await session.prepareMainProject(); - //remove the breakpoint breakpoint (it should not remove the breakpoint because it was already verified) + //remove the breakpoint args.breakpoints = []; - session.setBreakPointsRequest({}, args); + await session.setBreakPointsRequest({}, args); expect(response.body.breakpoints).to.be.lengthOf(0); - //add breakpoint during live debug session. one was there before, the other is new. Only one will be verified + //add breakpoint during live debug session. one was there before, the other is new. Neither will be verified right now args.breakpoints = [{ line: 1 }, { line: 2 }]; - session.setBreakPointsRequest({}, args); + await session.setBreakPointsRequest({}, args); expect( response.body.breakpoints.map(x => ({ line: x.line, verified: x.verified })) ).to.eql([{ @@ -397,18 +746,21 @@ describe('BrightScriptDebugSession', () => { }]); }); - it('supports breakpoints within xml files', () => { + it('supports breakpoints within xml files', async () => { args.source.path = `${rootDir}/some/xml-file.xml`; args.breakpoints = [{ line: 1 }]; - session.setBreakPointsRequest({}, args); - //breakpoint should be disabled - expect(response.body.breakpoints[0]).to.deep.include({ line: 1, verified: true }); + await session.setBreakPointsRequest({}, args); + //breakpoint should be unverified by default + expect(response.body.breakpoints[0]).to.deep.include({ + line: 1, + verified: false + }); }); - it('handles breakpoints for non-brightscript files', () => { + it('handles breakpoints for non-brightscript files', async () => { args.source.path = `${rootDir}/some/xml-file.jpg`; args.breakpoints = [{ line: 1 }]; - session.setBreakPointsRequest({}, args); + await session.setBreakPointsRequest({}, args); expect(response.body.breakpoints).to.be.lengthOf(1); //breakpoint should be disabled expect(response.body.breakpoints[0]).to.deep.include({ line: 1, verified: false }); @@ -419,12 +771,12 @@ describe('BrightScriptDebugSession', () => { it('registers the entry breakpoint when stopOnEntry is enabled', async () => { (session as any).launchConfiguration = { stopOnEntry: true }; session.projectManager.mainProject = { - stagingFolderPath: stagingFolderPath + stagingDir: stagingDir }; let stub = sinon.stub(session.projectManager, 'registerEntryBreakpoint').returns(Promise.resolve()); await session.handleEntryBreakpoint(); expect(stub.called).to.be.true; - expect(stub.args[0][0]).to.equal(stagingFolderPath); + expect(stub.args[0][0]).to.equal(stagingDir); }); it('does NOT register the entry breakpoint when stopOnEntry is enabled', async () => { (session as any).launchConfiguration = { stopOnEntry: false }; @@ -435,21 +787,19 @@ describe('BrightScriptDebugSession', () => { }); describe('shutdown', () => { - it('erases all staging folders when configured to do so', () => { + it('erases all staging folders when configured to do so', async () => { let stub = sinon.stub(fsExtra, 'removeSync').returns(null); session.projectManager.mainProject = { - stagingFolderPath: 'stagingPathA' + stagingDir: 'stagingPathA' }; session.projectManager.componentLibraryProjects.push({ - stagingFolderPath: 'stagingPathB' + stagingDir: 'stagingPathB' }); (session as any).launchConfiguration = { retainStagingFolder: false }; - //stub the super shutdown call so it doesn't kill the test session - sinon.stub(DebugSession.prototype, 'shutdown').returns(null); - session.shutdown(); + await session.shutdown(); expect(stub.callCount).to.equal(2); expect(stub.args.map(x => x[0])).to.eql([ 'stagingPathA', @@ -458,6 +808,37 @@ describe('BrightScriptDebugSession', () => { }); }); + describe('handleDiagnostics', () => { + it('finds source location for file-only path', async () => { + session['rokuAdapter'] = { destroy: () => { } } as any; + session.projectManager.mainProject = new Project({ + rootDir: rootDir, + outDir: stagingDir + } as Partial as any); + session.projectManager['mainProject'].fileMappings = []; + + fsExtra.outputFileSync(`${stagingDir}/.roku-deploy-staging/components/SomeComponent.xml`, ''); + fsExtra.outputFileSync(`${rootDir}/components/SomeComponent.xml`, ''); + + const stub = sinon.stub(session, 'sendEvent').callsFake(() => { }); + await session['handleDiagnostics']([{ + message: 'Crash', + path: 'SomeComponent.xml', + 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), + severity: DiagnosticSeverity.Warning, + source: 'roku-debug' + }] + }); + }); + }); + describe('evaluateRequest', () => { const frameId = 12; let evalStub: SinonStub; @@ -469,13 +850,15 @@ describe('BrightScriptDebugSession', () => { } as EvaluateContainer; beforeEach(() => { + session['rokuAdapterDeferred'].resolve(session['rokuAdapter']); + rokuAdapter.isAtDebuggerPrompt = true; evalStub = sinon.stub(rokuAdapter, 'evaluate').callsFake((args) => { console.log('called with', args); - return { + return Promise.resolve({ message: undefined, type: 'message' - }; + }); }); getVarStub = sinon.stub(rokuAdapter, 'getVariable').callsFake(() => { return Promise.resolve(getVarValue); @@ -493,15 +876,15 @@ describe('BrightScriptDebugSession', () => { it('ensures closing quote for hover', async () => { initRequestArgs.supportsInvalidatedEvent = true; - await expectResponse({ + const result = await session.evaluateRequest({} as any, { + frameId: frameId, expression: `"Billy`, context: 'hover' - }, { - result: 'invalid', - variablesReference: 0 }); console.log('checking calls'); - expect(evalStub.getCall(0)?.args[0]).equal('"Billy"'); + expect( + evalStub.getCalls().find(x => x.args.find(x => x?.toString().includes('"Billy"'))) + ).to.exist; }); it('skips when not at debugger prompt', async () => { @@ -551,6 +934,42 @@ describe('BrightScriptDebugSession', () => { expect(session['variables']).to.be.empty; }); + describe('stackTraceRequest', () => { + it('gracefully handles missing files', async () => { + session.projectManager.mainProject = new Project({ + rootDir: rootDir, + outDir: stagingDir + } as Partial as any); + session.projectManager['mainProject'].fileMappings = []; + + session.projectManager.componentLibraryProjects.push( + new ComponentLibraryProject({ + rootDir: complib1Dir, + stagingDir: stagingDir, + outDir: outDir, + libraryIndex: 1 + } as Partial as any) + ); + session.projectManager['componentLibraryProjects'][0].fileMappings = []; + + sinon.stub(rokuAdapter, 'getStackTrace').returns(Promise.resolve([{ + filePath: 'customComplib:/source/lib/AdManager__lib1.brs', + lineNumber: 500, + functionIdentifier: 'doSomething' + }, { + filePath: 'roku_ads_lib:/libsource/Roku_Ads.brs', + lineNumber: 400, + functionIdentifier: 'roku_ads__showads' + }, { + filePath: 'pkg:/source/main.brs', + lineNumber: 10, + functionIdentifier: 'main' + }] as StackFrame[])); + await session['stackTraceRequest']({} as any, { threadId: 1 }); + expect(errorSpy.getCalls()[0]?.args ?? []).to.eql([]); + }); + }); + describe('repl', () => { it('calls eval for print statement', async () => { await expectResponse({ @@ -577,7 +996,5 @@ describe('BrightScriptDebugSession', () => { expect(getVarStub.calledWith('person.name', frameId, true)); }); }); - }); - }); diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index 3b593236..69cfbcc6 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -1,10 +1,10 @@ import * as fsExtra from 'fs-extra'; import { orderBy } from 'natural-orderby'; import * as path from 'path'; -import * as request from 'request'; -import * as rokuDeploy from 'roku-deploy'; -import type { RokuDeploy, RokuDeployOptions } from 'roku-deploy'; +import { rokuDeploy, CompileError } from 'roku-deploy'; +import type { DeviceInfo, RokuDeploy, RokuDeployOptions } from 'roku-deploy'; import { + BreakpointEvent, DebugSession as BaseDebugSession, Handles, InitializedEvent, @@ -26,25 +26,36 @@ 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 { BrightScriptDebugCompileError } from '../CompileErrorProcessor'; +import type { BSDebugDiagnostic } from '../CompileErrorProcessor'; +import { RendezvousTracker } from '../RendezvousTracker'; import { LaunchStartEvent, LogOutputEvent, RendezvousEvent, - CompileFailureEvent, + DiagnosticsEvent, StoppedEventReason, ChanperfEvent, - DebugServerLogOutputEvent + DebugServerLogOutputEvent, + ChannelPublishedEvent, + PopupMessageEvent, + CustomRequestEvent, + ClientToServerCustomEventName } from './Events'; import type { LaunchConfiguration, ComponentLibraryConfiguration } from '../LaunchConfiguration'; import { FileManager } from '../managers/FileManager'; import { SourceMapManager } from '../managers/SourceMapManager'; import { LocationManager } from '../managers/LocationManager'; +import type { AugmentedSourceBreakpoint } from '../managers/BreakpointManager'; import { BreakpointManager } from '../managers/BreakpointManager'; import type { LogMessage } from '../logging'; -import { logger, debugServerLogOutputEventTransport } from '../logging'; +import { logger, FileLoggingManager, debugServerLogOutputEventTransport, LogLevelPriority } from '../logging'; +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() { @@ -60,10 +71,30 @@ export class BrightScriptDebugSession extends BaseDebugSession { this.sourceMapManager = new SourceMapManager(); 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.onDeviceBreakpointsChanged('changed', data)); this.projectManager = new ProjectManager(this.breakpointManager, this.locationManager); + this.fileLoggingManager = new FileLoggingManager(); } - public logger = logger.createLogger(`[${BrightScriptDebugSession.name}]`); + 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: breakpoint.verified, + id: breakpoint.id, + source: { + path: breakpoint.srcPath + } + }; + this.sendEvent(new BreakpointEvent(eventName, event)); + } + } + + public logger = logger.createLogger(`[session]`); /** * A sequence used to help identify log statements for requests @@ -74,6 +105,8 @@ export class BrightScriptDebugSession extends BaseDebugSession { public projectManager: ProjectManager; + public fileLoggingManager: FileLoggingManager; + public breakpointManager: BreakpointManager; public locationManager: LocationManager; @@ -99,8 +132,28 @@ export class BrightScriptDebugSession extends BaseDebugSession { private variableHandles = new Handles(); private rokuAdapter: DebugProtocolAdapter | TelnetAdapter; - private enableDebugProtocol: boolean; + private rendezvousTracker: RendezvousTracker; + + 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; + } + + /** + * Get a promise that resolves when the roku adapter is ready to be used + */ private getRokuAdapter() { return this.rokuAdapterDeferred.promise; } @@ -156,19 +209,97 @@ export class BrightScriptDebugSession extends BaseDebugSession { this.logger.log('initializeRequest finished'); } + private showPopupMessage(message: string, severity: 'error' | 'warn' | 'info', modal = false) { + this.logger.trace('[showPopupMessage]', severity, message); + this.sendEvent(new PopupMessageEvent(message, severity, modal)); + } + + private static requestIdSequence = 0; + + private async sendCustomRequest(name: string, data: T) { + const requestId = BrightScriptDebugSession.requestIdSequence++; + const responsePromise = new Promise((resolve, reject) => { + this.on(ClientToServerCustomEventName.customRequestEventResponse, (response) => { + if (response.requestId === requestId) { + if (response.error) { + throw response.error; + } else { + resolve(response); + } + } + }); + }); + this.sendEvent( + new CustomRequestEvent({ + requestId: requestId, + name: name, + ...data ?? {} + })); + await responsePromise; + } + + /** + * Get the cwd from the launchConfiguration, or default to process.cwd() + */ + private get cwd() { + return this.launchConfiguration?.cwd ?? process.cwd(); + } + + public deviceInfo: DeviceInfo; + + /** + * Set defaults and standardize values for all of the LaunchConfiguration values + * @param config + * @returns + */ + private normalizeLaunchConfig(config: LaunchConfiguration) { + config.cwd ??= process.cwd(); + config.outDir ??= s`${config.cwd}/out`; + config.stagingDir ??= s`${config.outDir}/.roku-deploy-staging`; + config.componentLibrariesPort ??= 8080; + config.packagePort ??= 80; + config.remotePort ??= 8060; + config.sceneGraphDebugCommandsPort ??= 8080; + config.controlPort ??= 8081; + config.brightScriptConsolePort ??= 8085; + config.stagingDir ??= config.stagingFolderPath; + config.emitChannelPublishedEvent ??= true; + return config; + } + public async launchRequest(response: DebugProtocol.LaunchResponse, config: LaunchConfiguration) { this.logger.log('[launchRequest] begin'); - this.launchConfiguration = config; + + //send the response right away so the UI immediately shows the debugger toolbar + this.sendResponse(response); + + this.launchConfiguration = this.normalizeLaunchConfig(config); //set the logLevel provided by the launch config if (this.launchConfiguration.logLevel) { logger.logLevel = this.launchConfiguration.logLevel; } - this.enableDebugProtocol = this.launchConfiguration.enableDebugProtocol; - //do a DNS lookup for the host to fix issues with roku rejecting ECP - this.launchConfiguration.host = await util.dnsLookup(this.launchConfiguration.host); + try { + this.launchConfiguration.host = await util.dnsLookup(this.launchConfiguration.host); + } catch (e) { + return this.shutdown(`Could not resolve ip address for host '${this.launchConfiguration.host}'`); + } + + // fetches the device info and parses the xml data to JSON object + try { + this.deviceInfo = await rokuDeploy.getDeviceInfo({ host: this.launchConfiguration.host, remotePort: this.launchConfiguration.remotePort, enhance: true, timeout: 4_000 }); + } catch (e) { + return this.shutdown(`Unable to connect to roku at '${this.launchConfiguration.host}'. Verify the IP address is correct and that the device is powered on and connected to same network as this computer.`); + } + + if (this.deviceInfo && !this.deviceInfo.developerEnabled) { + return this.shutdown(`Developer mode is not enabled for host '${this.launchConfiguration.host}'.`); + } + + //initialize all file logging (rokuDevice, debugger, etc) + this.fileLoggingManager.activate(this.launchConfiguration?.fileLogging, this.cwd); this.projectManager.launchConfiguration = this.launchConfiguration; this.breakpointManager.launchConfiguration = this.launchConfiguration; @@ -186,28 +317,22 @@ export class BrightScriptDebugSession extends BaseDebugSession { ]); this.logger.log(`Packaging projects took: ${(util.formatTime(Date.now() - start))}`); - util.log(`Connecting to Roku via ${this.enableDebugProtocol ? 'the BrightScript debug protocol' : 'telnet'} at ${this.launchConfiguration.host}`); - - this.createRokuAdapter(this.launchConfiguration.host); - if (!this.enableDebugProtocol) { - //connect to the roku debug via telnet - if (!this.rokuAdapter.connected) { - await this.connectRokuAdapter(); - } + if (this.enableDebugProtocol) { + util.log(`Connecting to Roku via the BrightScript debug protocol at ${this.launchConfiguration.host}:${this.launchConfiguration.controlPort}`); } else { - await (this.rokuAdapter as DebugProtocolAdapter).watchCompileOutput(); + util.log(`Connecting to Roku via telnet at ${this.launchConfiguration.host}:${this.launchConfiguration.brightScriptConsolePort}`); } + await this.initRendezvousTracking(); + + this.createRokuAdapter(this.rendezvousTracker); + await this.connectRokuAdapter(); + await this.runAutomaticSceneGraphCommands(this.launchConfiguration.autoRunSgDebugCommands); //press the home button to ensure we're at the home screen await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort); - //pass the debug functions used to locate the client files and lines thought the adapter to the RendezvousTracker - this.rokuAdapter.registerSourceLocator(async (debuggerPath: string, lineNumber: number) => { - return this.projectManager.getSourceLocation(debuggerPath, lineNumber); - }); - //pass the log level down thought the adapter to the RendezvousTracker and ChanperfTracker this.rokuAdapter.setConsoleOutput(this.launchConfiguration.consoleOutput); @@ -227,11 +352,6 @@ export class BrightScriptDebugSession extends BaseDebugSession { this.sendEvent(new ChanperfEvent(output)); }); - // Send rendezvous events to the extension - this.rokuAdapter.on('rendezvous', (output) => { - this.sendEvent(new RendezvousEvent(output)); - }); - //listen for a closed connection (shut down when received) this.rokuAdapter.on('close', (reason = '') => { if (reason === 'compileErrors') { @@ -242,46 +362,23 @@ export class BrightScriptDebugSession extends BaseDebugSession { }); // handle any compile errors - // eslint-disable-next-line @typescript-eslint/no-misused-promises - this.rokuAdapter.on('compile-errors', async (errors: BrightScriptDebugCompileError[]) => { - // remove redundant errors and adjust the line number: - // - Roku device and sourcemap work with 1-based line numbers, - // - VS expects 0-based lines. - const compileErrors = util.filterGenericErrors(errors); - for (let compileError of compileErrors) { - let sourceLocation = await this.projectManager.getSourceLocation(compileError.path, compileError.lineNumber); - if (sourceLocation) { - compileError.path = sourceLocation.filePath; - compileError.lineNumber = sourceLocation.lineNumber - 1; //0-based - } else { - // TODO: may need to add a custom event if the source location could not be found by the ProjectManager - compileError.path = fileUtils.removeLeadingSlash(util.removeFileScheme(compileError.path)); - compileError.lineNumber = (compileError.lineNumber || 1) - 1; //0-based - } - } - - this.sendEvent(new CompileFailureEvent(compileErrors)); - //stop the roku adapter and exit the channel - void this.rokuAdapter.destroy(); - void this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort); + this.rokuAdapter.on('diagnostics', (diagnostics: BSDebugDiagnostic[]) => { + void this.handleDiagnostics(diagnostics); }); // close disconnect if required when the app is exited // eslint-disable-next-line @typescript-eslint/no-misused-promises this.rokuAdapter.on('app-exit', async () => { - if (this.launchConfiguration.stopDebuggerOnAppExit || !this.rokuAdapter.supportsMultipleRuns) { - let message = `App exit event detected${this.rokuAdapter.supportsMultipleRuns ? ' and launchConfiguration.stopDebuggerOnAppExit is true' : ''}`; + this.entryBreakpointWasHandled = false; + this.breakpointManager.clearBreakpointLastState(); + + if (this.launchConfiguration.stopDebuggerOnAppExit) { + let message = `App exit event detected and launchConfiguration.stopDebuggerOnAppExit is true`; message += ' - shutting down debug session'; this.logger.log('on app-exit', message); this.sendEvent(new LogOutputEvent(message)); - if (this.rokuAdapter) { - void this.rokuAdapter.destroy(); - } - //return to the home screen - await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort); - this.shutdown(); - this.sendEvent(new TerminatedEvent()); + await this.shutdown(); } else { const message = 'App exit detected; but launchConfiguration.stopDebuggerOnAppExit is set to false, so keeping debug session running.'; this.logger.log('[launchRequest]', message); @@ -289,33 +386,29 @@ 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.publish(); - //publish the package to the target Roku - await this.rokuDeploy.publish(this.launchConfiguration as any as RokuDeployOptions); - - if (this.enableDebugProtocol) { - //connect to the roku debug via sockets - await this.connectRokuAdapter(); + //hack for certain roku devices that lock up when this event is emitted (no idea why!). + if (this.launchConfiguration.emitChannelPublishedEvent) { + this.sendEvent(new ChannelPublishedEvent( + this.launchConfiguration + )); } //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); } }); } @@ -324,19 +417,14 @@ export class BrightScriptDebugSession extends BaseDebugSession { } } catch (e) { //if the message is anything other than compile errors, we want to display the error - //TODO: look into the reason why we are getting the 'Invalid response code: 400' on compile errors - if (e.message !== 'compileErrors' && e.message !== 'Invalid response code: 400') { - //TODO make the debugger stop! + 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); - } else { - //request adapter to send errors (even empty) before ending the session - await this.rokuAdapter.sendErrors(); + 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); - this.shutdown(); - return; } //at this point, the project has been deployed. If we need to use a deep link, launch it now. @@ -346,13 +434,157 @@ export class BrightScriptDebugSession extends BaseDebugSession { //if we are at a breakpoint, continue await this.rokuAdapter.continue(); //kill the app on the roku - await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort); + // await this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort); + //convert a hostname to an ip address + const deepLinkUrl = await util.resolveUrl(this.launchConfiguration.deepLinkUrl); //send the deep link http request - await new Promise((resolve, reject) => { - request.post(this.launchConfiguration.deepLinkUrl, (err, response) => { - return err ? reject(err) : resolve(response); - }); - }); + await util.httpPost(deepLinkUrl); + } + } + + /** + * Activate rendezvous tracking (IF enabled in the LaunchConfig) + */ + public async initRendezvousTracking() { + const timeout = 5000; + let initCompleted = false; + await Promise.race([ + util.sleep(timeout), + this._initRendezvousTracking().finally(() => { + initCompleted = true; + }) + ]); + + if (initCompleted === false) { + this.showPopupMessage(`Rendezvous tracking timed out after ${timeout}ms. Consider setting "rendezvousTracking": false in launch.json`, 'warn'); + } + } + + private async _initRendezvousTracking() { + this.rendezvousTracker = new RendezvousTracker(this.deviceInfo, this.launchConfiguration); + + //pass the debug functions used to locate the client files and lines thought the adapter to the RendezvousTracker + this.rendezvousTracker.registerSourceLocator(async (debuggerPath: string, lineNumber: number) => { + return this.projectManager.getSourceLocation(debuggerPath, lineNumber); + }); + + // Send rendezvous events to the debug protocol client + this.rendezvousTracker.on('rendezvous', (output) => { + this.sendEvent(new RendezvousEvent(output)); + }); + + //clear the history so the user doesn't have leftover rendezvous data from a previous session + this.rendezvousTracker.clearHistory(); + + //if rendezvous tracking is enabled, then enable it on the device + if (this.launchConfiguration.rendezvousTracking !== false) { + // start ECP rendezvous tracking (if possible) + await this.rendezvousTracker.activate(); + } + } + + /** + * Anytime a roku adapter emits diagnostics, this method is called to handle it. + */ + 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; + diagnostic.range.start.line = sourceLocation.lineNumber - 1; //sourceLocation is 1-based, but we need 0-based + diagnostic.range.end.line = sourceLocation.lineNumber - 1; //sourceLocation is 1-based, but we need 0-based + } else { + // TODO: may need to add a custom event if the source location could not be found by the ProjectManager + diagnostic.path = fileUtils.removeLeadingSlash(util.removeFileScheme(diagnostic.path)); + } + } + + //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)); + } + + private async publish() { + this.logger.log('Uploading zip'); + const start = Date.now(); + let packageIsPublished = false; + + //delete any currently installed dev channel (if enabled to do so) + try { + if (this.launchConfiguration.deleteDevChannelBeforeInstall === true) { + await this.rokuDeploy.deleteInstalledChannel({ + ...this.launchConfiguration + } as any as RokuDeployOptions); + } + } catch (e) { + const statusCode = e?.results?.response?.statusCode; + const message = e.message as string; + if (statusCode === 401) { + this.showPopupMessage(message, 'error', true); + await this.shutdown(message); + throw e; + } + this.logger.warn('Failed to delete the dev channel...probably not a big deal', e); + } + + const isConnected = this.rokuAdapter.once('app-ready'); + const options: RokuDeployOptions = { + ...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, + //pass any upload form overrides the client may have configured + packageUploadOverrides: this.launchConfiguration.packageUploadOverrides + }; + //if packagePath is specified, use that info instead of outDir and outFile + if (this.launchConfiguration.packagePath) { + options.outDir = path.dirname(this.launchConfiguration.packagePath); + options.outFile = path.basename(this.launchConfiguration.packagePath); + } + + //publish the package to the target Roku + const publishPromise = this.rokuDeploy.publish(options).then(() => { + packageIsPublished = true; + }).catch(async (e) => { + const statusCode = e?.results?.response?.statusCode; + const message = e.message as string; + if (statusCode && statusCode !== 200) { + this.showPopupMessage(message, 'error', true); + await this.shutdown(message); + throw e; + } + this.logger.error(e); + }); + + await publishPromise; + + this.logger.log(`Uploading zip took ${Date.now() - start}ms`); + + //the channel has been deployed. Wait for the adapter to finish connecting. + //if it hasn't connected after 5 seconds, it probably will never connect. + await Promise.race([ + isConnected, + util.sleep(10_000) + ]); + this.logger.log('Finished racing promises'); + //if the adapter is still not connected, then it will probably never connect. Abort. + if (packageIsPublished && !this.rokuAdapter.connected) { + return this.shutdown('Debug session cancelled: failed to connect to debug protocol control port.'); } } @@ -361,6 +593,7 @@ export class BrightScriptDebugSession extends BaseDebugSession { * @param logOutput */ private sendLogOutput(logOutput: string) { + this.fileLoggingManager.writeRokuDeviceLog(logOutput); const lines = logOutput.split(/\r?\n/g); for (let line of lines) { line += '\n'; @@ -371,7 +604,7 @@ export class BrightScriptDebugSession extends BaseDebugSession { private async runAutomaticSceneGraphCommands(commands: string[]) { if (commands) { - let connection = new SceneGraphDebugCommandController(this.launchConfiguration.host); + let connection = new SceneGraphDebugCommandController(this.launchConfiguration.host, this.launchConfiguration.sceneGraphDebugCommandsPort); try { await connection.connect(); @@ -433,7 +666,8 @@ export class BrightScriptDebugSession extends BaseDebugSession { raleTrackerTaskFileLocation: this.launchConfiguration.raleTrackerTaskFileLocation, injectRdbOnDeviceComponent: this.launchConfiguration.injectRdbOnDeviceComponent, rdbFilesBasePath: this.launchConfiguration.rdbFilesBasePath, - stagingFolderPath: this.launchConfiguration.stagingFolderPath + stagingDir: this.launchConfiguration.stagingDir, + packagePath: this.launchConfiguration.packagePath }); util.log('Moving selected files to staging area'); @@ -445,29 +679,51 @@ export class BrightScriptDebugSession extends BaseDebugSession { //add breakpoint lines to source files and then publish util.log('Adding stop statements for active breakpoints'); - //prevent new breakpoints from being verified - this.breakpointManager.lockBreakpoints(); + //write the `stop` statements to every file that has breakpoints (do for telnet, skip for debug protocol) + if (!this.enableDebugProtocol) { - //write all `stop` statements to the files in the staging folder - await this.breakpointManager.writeBreakpointsForProject(this.projectManager.mainProject); + await this.breakpointManager.writeBreakpointsForProject(this.projectManager.mainProject); + } + + if (this.launchConfiguration.packageTask) { + util.log(`Executing task '${this.launchConfiguration.packageTask}' to assemble the app`); + await this.sendCustomRequest('executeTask', { task: this.launchConfiguration.packageTask }); + + const options = { + ...this.launchConfiguration + } as any as RokuDeployOptions; + //if packagePath is specified, use that info instead of outDir and outFile + if (this.launchConfiguration.packagePath) { + options.outDir = path.dirname(this.launchConfiguration.packagePath); + options.outFile = path.basename(this.launchConfiguration.packagePath); + } + const packagePath = this.launchConfiguration.packagePath ?? rokuDeploy.getOutputZipFilePath(options); - //create zip package from staging folder - util.log('Creating zip archive from project sources'); - await this.projectManager.mainProject.zipPackage({ retainStagingFolder: true }); + if (!fsExtra.pathExistsSync(packagePath)) { + return this.shutdown(`Cancelling debug session. Package does not exist at '${packagePath}'`); + } + } else { + //create zip package from staging folder + util.log('Creating zip archive from project sources'); + await this.projectManager.mainProject.zipPackage({ retainStagingFolder: true }); + } } /** * Accepts custom events and requests from the extension * @param command name of the command to execute */ - protected customRequest(command: string) { + protected customRequest(command: string, response: DebugProtocol.Response, args: any) { if (command === 'rendezvous.clearHistory') { this.rokuAdapter.clearRendezvousHistory(); - } - if (command === 'chanperf.clearHistory') { + } else if (command === 'chanperf.clearHistory') { this.rokuAdapter.clearChanperfHistory(); + + } else if (command === 'customRequestEventResponse') { + this.emit('customRequestEventResponse', args); } + this.sendResponse(response); } /** @@ -507,8 +763,10 @@ export class BrightScriptDebugSession extends BaseDebugSession { // Add breakpoint lines to the staging files and before publishing util.log('Adding stop statements for active breakpoints in Component Libraries'); - //write the `stop` statements to every file that has breakpoints - await this.breakpointManager.writeBreakpointsForProject(compLibProject); + //write the `stop` statements to every file that has breakpoints (do for telnet, skip for debug protocol) + if (!this.enableDebugProtocol) { + await this.breakpointManager.writeBreakpointsForProject(compLibProject); + } await compLibProject.postfixFiles(); @@ -548,32 +806,18 @@ 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 setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments) { + public async setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments) { + this.logger.log('setBreakpointsRequest', args); let sanitizedBreakpoints = this.breakpointManager.replaceBreakpoints(args.source.path, args.breakpoints); //sort the breakpoints - let sortedAndFilteredBreakpoints = orderBy(sanitizedBreakpoints, [x => x.line, x => x.column]) - //filter out the inactive breakpoints - .filter(x => x.isHidden === false); + let sortedAndFilteredBreakpoints = orderBy(sanitizedBreakpoints, [x => x.line, x => x.column]); response.body = { breakpoints: sortedAndFilteredBreakpoints }; this.sendResponse(response); - //set a small timeout so the user sees the breakpoints disappear before reappearing - //This is disabled because I'm not sure anyone actually wants this functionality, but I didn't want to lose it. - // setTimeout(() => { - // //notify the client about every other breakpoint that was not explicitly requested here - // //(basically force to re-enable the `stop` breakpoints that were written into the source code by the debugger) - // var otherBreakpoints = sanitizedBreakpoints.filter(x => sortedAndFilteredBreakpoints.indexOf(x) === -1); - // for (var breakpoint of otherBreakpoints) { - // this.sendEvent(new BreakpointEvent('new', { - // line: breakpoint.line, - // verified: true, - // source: args.source - // })); - // } - // }, 100); + await this.rokuAdapter?.syncBreakpoints(); } protected exceptionInfoRequest(response: DebugProtocol.ExceptionInfoResponse, args: DebugProtocol.ExceptionInfoArguments) { @@ -582,22 +826,38 @@ export class BrightScriptDebugSession extends BaseDebugSession { protected async threadsRequest(response: DebugProtocol.ThreadsResponse) { this.logger.log('threadsRequest'); - //wait for the roku adapter to load - await this.getRokuAdapter(); 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 { + //wait for the roku adapter to load + await this.getRokuAdapter(); - for (let thread of rokuThreads) { - threads.push( - new Thread(thread.threadId, `Thread ${thread.threadId}`) - ); + //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}`) + ); + } + + if (threads.length === 0) { + threads = [{ + id: 1001, + name: 'unable to retrieve threads: not stopped', + isFake: true + }]; + } + + } 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 = { @@ -610,46 +870,74 @@ export class BrightScriptDebugSession extends BaseDebugSession { protected async stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) { try { 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 - ); + let frames: DebugProtocol.StackFrame[] = []; + + //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 (args.threadId === 1001) { + frames.push(new StackFrame( + 0, + 'ERROR: threads would not stop', + new Source('main.brs', s`${this.launchConfiguration.stagingDir}/manifest`), + 1, + 1 + )); + this.showPopupMessage('Unable to suspend threads. Debugger is in an unstable state, please press Continue to resume debugging', 'warn'); + } else { + //ensure the rokuAdapter is loaded + await this.getRokuAdapter(); + + 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 }); + } + 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'; } - } catch (error) { - this.logger.error('Error correcting function identifier case', { error, sourceLocation, debugFrame }); + frames.push(frame); } - - let frame = new StackFrame( - debugFrame.frameId, - `${debugFrame.functionIdentifier}`, - new Source(path.basename(sourceLocation.filePath), sourceLocation.filePath), - sourceLocation.lineNumber, - 1 - ); - 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, @@ -667,14 +955,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`); } @@ -704,6 +992,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); @@ -711,6 +1006,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); } @@ -727,6 +1030,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'); @@ -738,6 +1049,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'); @@ -745,6 +1064,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'); @@ -761,63 +1088,94 @@ export class BrightScriptDebugSession extends BaseDebugSession { try { logger.log('begin', { args }); + //ensure the rokuAdapter is loaded + await this.getRokuAdapter(); + let childVariables: AugmentedVariable[] = []; //wait for any `evaluate` commands to finish so we have a higher likely hood of being at a debugger prompt await this.evaluateRequestPromise; - if (this.rokuAdapter.isAtDebuggerPrompt) { - const reference = this.variableHandles.get(args.variablesReference); - if (reference) { - logger.log('reference', reference); - // NOTE: Legacy telnet support for local vars - if (this.launchConfiguration.enableVariablesPanel) { - const vars = await (this.rokuAdapter as TelnetAdapter).getScopeVariables(reference); - - for (const varName of vars) { - let result = await this.rokuAdapter.getVariable(varName, -1); - let tempVar = this.getVariableFromResult(result, -1); - childVariables.push(tempVar); - } - } else { - childVariables.push(new Variable('variables disabled by launch.json setting', 'enableVariablesPanel: false')); + if (this.rokuAdapter?.isAtDebuggerPrompt !== true) { + logger.log('Skipped getting variables because the RokuAdapter is not accepting input at this time'); + response.success = false; + response.message = 'Debug session is not paused'; + return this.sendResponse(response); + } + const reference = this.variableHandles.get(args.variablesReference); + if (reference) { + logger.log('reference', reference); + // NOTE: Legacy telnet support for local vars + if (this.launchConfiguration.enableVariablesPanel) { + const vars = await (this.rokuAdapter as TelnetAdapter).getScopeVariables(); + + for (const varName of vars) { + let result = await this.rokuAdapter.getVariable(varName, -1); + let tempVar = this.getVariableFromResult(result, -1); + childVariables.push(tempVar); } } else { - //find the variable with this reference - let v = this.variables[args.variablesReference]; - logger.log('variable', v); - //query for child vars if we haven't done it yet. - if (v.childVariables.length === 0) { - let result = await this.rokuAdapter.getVariable(v.evaluateName, v.frameId); - let tempVar = this.getVariableFromResult(result, v.frameId); - tempVar.frameId = v.frameId; - v.childVariables = tempVar.childVariables; - } - childVariables = v.childVariables; + childVariables.push(new Variable('variables disabled by launch.json setting', 'enableVariablesPanel: false')); } - - //if the variable is an array, send only the requested range - if (Array.isArray(childVariables) && args.filter === 'indexed') { - //only send the variable range requested by the debugger - childVariables = childVariables.slice(args.start, args.start + args.count); - } - response.body = { - variables: childVariables - }; } else { - logger.log('Skipped getting variables because the RokuAdapter is not accepting input at this time'); + //find the variable with this reference + let v = this.variables[args.variablesReference]; + if (!v) { + response.success = false; + response.message = `Variable reference has expired`; + return this.sendResponse(response); + } + logger.log('variable', v); + //query for child vars if we haven't done it yet. + if (v.childVariables.length === 0) { + let result = await this.rokuAdapter.getVariable(v.evaluateName, v.frameId); + let tempVar = this.getVariableFromResult(result, v.frameId); + tempVar.frameId = v.frameId; + v.childVariables = tempVar.childVariables; + } + childVariables = v.childVariables; } - logger.info('end', { response }); - this.sendResponse(response); + + //if the variable is an array, send only the requested range + if (Array.isArray(childVariables) && args.filter === 'indexed') { + //only send the variable range requested by the debugger + childVariables = childVariables.slice(args.start, args.start + args.count); + } + + let filteredChildVariables = this.launchConfiguration.showHiddenVariables !== true ? childVariables.filter( + (child: AugmentedVariable) => !child.name.startsWith(this.tempVarPrefix)) : childVariables; + + response.body = { + variables: filteredChildVariables + }; } catch (error) { logger.error('Error during variablesRequest', error, { args }); + response.success = false; + response.message = error?.message ?? 'Error during variablesRequest'; + } finally { + logger.info('end', { response }); } + this.sendResponse(response); } private evaluateRequestPromise = Promise.resolve(); + private evaluateVarIndexByFrameId = new Map(); + + private getNextVarIndex(frameId: number): number { + if (!this.evaluateVarIndexByFrameId.has(frameId)) { + this.evaluateVarIndexByFrameId.set(frameId, 0); + } + let value = this.evaluateVarIndexByFrameId.get(frameId); + this.evaluateVarIndexByFrameId.set(frameId, value + 1); + return value; + } public async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) { + //ensure the rokuAdapter is loaded + await this.getRokuAdapter(); + let deferred = defer(); if (args.context === 'repl' && !this.enableDebugProtocol && args.expression.trim().startsWith('>')) { this.clearState(); + this.rokuAdapter.clearCache(); const expression = args.expression.replace(/^\s*>\s*/, ''); this.logger.log('Sending raw telnet command...I sure hope you know what you\'re doing', { expression }); (this.rokuAdapter as TelnetAdapter).requestPipeline.client.write(`${expression}\r\n`); @@ -849,7 +1207,23 @@ export class BrightScriptDebugSession extends BaseDebugSession { //is at debugger prompt } else { - const variablePath = util.getVariablePath(args.expression); + let variablePath = util.getVariablePath(args.expression); + if (!variablePath && util.isAssignableExpression(args.expression)) { + let varIndex = this.getNextVarIndex(args.frameId); + let arrayVarName = this.tempVarPrefix + 'eval'; + if (varIndex === 0) { + 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()]; + } + //if we found a variable path (e.g. ['a', 'b', 'c']) then do a variable lookup because it's faster and more widely supported than `evaluate` if (variablePath) { let refId = this.getEvaluateRefId(args.expression, args.frameId); @@ -858,10 +1232,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 @@ -878,43 +1253,37 @@ export class BrightScriptDebugSession extends BaseDebugSession { //run an `evaluate` call } else { - if (args.context === 'repl' || !this.enableDebugProtocol) { - let commandResults = await this.rokuAdapter.evaluate(args.expression, args.frameId); - - commandResults.message = util.trimDebugPrompt(commandResults.message); - if (args.context !== 'watch') { - //clear variable cache since this action could have side-effects - this.clearState(); - this.sendInvalidatedEvent(null, args.frameId); - } - //if the adapter captured output (probably only telnet), print it to the vscode debug console - if (typeof commandResults.message === 'string') { - this.sendEvent(new OutputEvent(commandResults.message, commandResults.type === 'error' ? 'stderr' : 'stdio')); - } + let commandResults = await this.rokuAdapter.evaluate(args.expression, args.frameId); - if (this.enableDebugProtocol || (typeof commandResults.message !== 'string')) { - response.body = { - result: 'invalid', - variablesReference: 0 - }; - } else { - response.body = { - result: commandResults.message === '\r\n' ? 'invalid' : commandResults.message, - variablesReference: 0 - }; - } - } else { + commandResults.message = util.trimDebugPrompt(commandResults.message); + if (args.context !== 'watch') { + //clear variable cache since this action could have side-effects + this.clearState(); + this.sendInvalidatedEvent(null, args.frameId); + } + //if the adapter captured output (probably only telnet), print it to the vscode debug console + if (typeof commandResults.message === 'string') { + this.sendEvent(new OutputEvent(commandResults.message, commandResults.type === 'error' ? 'stderr' : 'stdio')); + } + + if (this.enableDebugProtocol || (typeof commandResults.message !== 'string')) { response.body = { result: 'invalid', variablesReference: 0 }; + } else { + response.body = { + result: commandResults.message === '\r\n' ? 'invalid' : commandResults.message, + variablesReference: 0 + }; } } } } catch (error) { this.logger.error('Error during variables request', error); + response.success = false; + response.message = error?.message ?? error; } - // try { this.sendResponse(response); } catch { } @@ -927,22 +1296,19 @@ 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) { + private createRokuAdapter(rendezvousTracker: RendezvousTracker) { if (this.enableDebugProtocol) { - this.rokuAdapter = new DebugProtocolAdapter(this.launchConfiguration); + this.rokuAdapter = new DebugProtocolAdapter(this.launchConfiguration, this.projectManager, this.breakpointManager, rendezvousTracker, this.deviceInfo); } else { - this.rokuAdapter = new TelnetAdapter(this.launchConfiguration); + this.rokuAdapter = new TelnetAdapter(this.launchConfiguration, rendezvousTracker); } } @@ -958,6 +1324,11 @@ export class BrightScriptDebugSession extends BaseDebugSession { await this.launchRequest(response, args.arguments as LaunchConfiguration); } + /** + * Used to track whether the entry breakpoint has already been handled + */ + private entryBreakpointWasHandled = false; + /** * Registers the main events for the RokuAdapter */ @@ -971,16 +1342,7 @@ export class BrightScriptDebugSession extends BaseDebugSession { //when the debugger suspends (pauses for debugger input) // eslint-disable-next-line @typescript-eslint/no-misused-promises this.rokuAdapter.on('suspend', async () => { - this.logger.info('received "suspend" event from adapter'); - let threads = await this.rokuAdapter.getThreads(); - let threadId = threads[0]?.threadId; - - this.clearState(); - let exceptionText = ''; - const event: StoppedEvent = new StoppedEvent(StoppedEventReason.breakpoint, threadId, exceptionText); - // Socket debugger will always stop all threads and supports multi thread inspection. - (event.body as any).allThreadsStopped = this.enableDebugProtocol; - this.sendEvent(event); + await this.onSuspend(); }); //anytime the adapter encounters an exception on the roku, @@ -994,11 +1356,73 @@ export class BrightScriptDebugSession extends BaseDebugSession { // If the roku says it can't continue, we are no longer able to debug, so kill the debug session this.rokuAdapter.on('cannot-continue', () => { - this.sendEvent(new TerminatedEvent()); + void this.shutdown(); }); + //make the connection await this.rokuAdapter.connect(); this.rokuAdapterDeferred.resolve(this.rokuAdapter); + return this.rokuAdapter; + } + + private async onSuspend() { + //clear the index for storing evalutated expressions + this.evaluateVarIndexByFrameId.clear(); + + const threads = await this.rokuAdapter.getThreads(); + const activeThread = threads.find(x => x.isSelected); + + //TODO remove this once Roku fixes their threads off-by-one line number issues + //look up the correct line numbers for each thread from the StackTrace + await Promise.all( + threads.map(async (thread) => { + const stackTrace = await this.rokuAdapter.getStackTrace(thread.threadId); + const stackTraceLineNumber = stackTrace[0]?.lineNumber; + if (stackTraceLineNumber !== thread.lineNumber) { + this.logger.warn(`Thread ${thread.threadId} reported incorrect line (${thread.lineNumber}). Using line from stack trace instead (${stackTraceLineNumber})`, thread, stackTrace); + thread.lineNumber = stackTraceLineNumber; + } + }) + ); + + 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; + //if there's a user-defined breakpoint at this exact position, it needs to be handled like a regular breakpoint (i.e. suspend). So only auto-continue if there's no breakpoint here + if (activeThread && !await this.breakpointManager.lineHasBreakpoint(this.projectManager.getAllProjects(), activeThread.filePath, activeThread.lineNumber - 1)) { + this.logger.info('Encountered entry breakpoint and `stopOnEntry` is disabled. Continuing...'); + return this.rokuAdapter.continue(); + } + } + + this.clearState(); + const event: StoppedEvent = new StoppedEvent( + StoppedEventReason.breakpoint, + //Not sure why, but sometimes there is no active thread. Just pick thread 0 to prevent the app from totally crashing + activeThread?.threadId ?? 0, + '' //exception text + ); + // Socket debugger will always stop all threads and supports multi thread inspection. + (event.body as any).allThreadsStopped = this.enableDebugProtocol; + this.sendEvent(event); + } private getVariableFromResult(result: EvaluateContainer, frameId: number) { @@ -1018,7 +1442,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 { @@ -1054,6 +1486,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; } @@ -1088,27 +1524,77 @@ export class BrightScriptDebugSession extends BaseDebugSession { * If `stopOnEntry` is enabled, register the entry breakpoint. */ public async handleEntryBreakpoint() { - if (this.launchConfiguration.stopOnEntry && !this.enableDebugProtocol) { - await this.projectManager.registerEntryBreakpoint(this.projectManager.mainProject.stagingFolderPath); + if (!this.enableDebugProtocol) { + this.entryBreakpointWasHandled = true; + if (this.launchConfiguration.stopOnEntry || this.launchConfiguration.deepLinkUrl) { + await this.projectManager.registerEntryBreakpoint(this.projectManager.mainProject.stagingDir); + } } } + private shutdownPromise: Promise | undefined = undefined; + /** - * Called when the debugger is terminated + * Called when the debugger is terminated. Feel free to call this as frequently as you want; we'll only run the shutdown process the first time, and return + * the same promise on subsequent calls */ - public shutdown() { - //if configured, delete the staging directory - if (!this.launchConfiguration.retainStagingFolder) { - let stagingFolderPaths = this.projectManager.getStagingFolderPaths(); - for (let stagingFolderPath of stagingFolderPaths) { - try { - fsExtra.removeSync(stagingFolderPath); - } catch (e) { - util.log(`Error removing staging directory '${stagingFolderPath}': ${JSON.stringify(e)}`); + public async shutdown(errorMessage?: string): Promise { + if (this.shutdownPromise === undefined) { + this.logger.log('[shutdown] Beginning shutdown sequence', errorMessage); + this.shutdownPromise = this._shutdown(errorMessage); + } else { + this.logger.log('[shutdown] Tried to call `.shutdown()` again. Returning the same promise'); + } + return this.shutdownPromise; + } + + private async _shutdown(errorMessage?: string): Promise { + try { + this.componentLibraryServer?.stop(); + + void this.rendezvousTracker?.destroy?.(); + + //if configured, delete the staging directory + if (!this.launchConfiguration.retainStagingFolder) { + const stagingDirs = this.projectManager?.getStagingDirs() ?? []; + this.logger.info('deleting staging folders', stagingDirs); + for (let stagingDir of stagingDirs) { + try { + fsExtra.removeSync(stagingDir); + } catch (e) { + this.logger.error(e); + util.log(`Error removing staging directory '${stagingDir}': ${JSON.stringify(e)}`); + } } } + + //if there was an error message, display it to the user + if (errorMessage) { + this.logger.error(errorMessage); + this.showPopupMessage(errorMessage, 'error'); + } + + 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()); + + //shut down the process + this.logger.log('super.shutdown()'); + super.shutdown(); + this.logger.log('shutdown complete'); + } catch (e) { + this.logger.error(e); } - super.shutdown(); } } diff --git a/src/debugSession/Events.spec.ts b/src/debugSession/Events.spec.ts new file mode 100644 index 00000000..8eddc1e6 --- /dev/null +++ b/src/debugSession/Events.spec.ts @@ -0,0 +1,26 @@ +import { expect } from 'chai'; +import { isDiagnosticsEvent, DiagnosticsEvent, isLogOutputEvent, LogOutputEvent, isDebugServerLogOutputEvent, DebugServerLogOutputEvent, isRendezvousEvent, RendezvousEvent, isChanperfEvent, ChanperfEvent, isLaunchStartEvent, LaunchStartEvent, isPopupMessageEvent, PopupMessageEvent, isChannelPublishedEvent, ChannelPublishedEvent } from './Events'; + +describe('Events', () => { + it('is* methods work properly', () => { + //match + expect(isDiagnosticsEvent(new DiagnosticsEvent(null))).to.be.true; + expect(isLogOutputEvent(new LogOutputEvent(null))).to.be.true; + expect(isDebugServerLogOutputEvent(new DebugServerLogOutputEvent(null))).to.be.true; + expect(isRendezvousEvent(new RendezvousEvent(null))).to.be.true; + expect(isChanperfEvent(new ChanperfEvent(null))).to.be.true; + expect(isLaunchStartEvent(new LaunchStartEvent(null))).to.be.true; + expect(isPopupMessageEvent(new PopupMessageEvent(null, 'error'))).to.be.true; + expect(isChannelPublishedEvent(new ChannelPublishedEvent(null))).to.be.true; + + //not match + expect(isDiagnosticsEvent(null)).to.be.false; + expect(isLogOutputEvent(null)).to.be.false; + expect(isDebugServerLogOutputEvent(null)).to.be.false; + expect(isRendezvousEvent(null)).to.be.false; + expect(isChanperfEvent(null)).to.be.false; + expect(isLaunchStartEvent(null)).to.be.false; + expect(isPopupMessageEvent(null)).to.be.false; + expect(isChannelPublishedEvent(null)).to.be.false; + }); +}); diff --git a/src/debugSession/Events.ts b/src/debugSession/Events.ts index c7039d93..66813d8f 100644 --- a/src/debugSession/Events.ts +++ b/src/debugSession/Events.ts @@ -1,73 +1,185 @@ +/* eslint-disable @typescript-eslint/no-useless-constructor */ import type { DebugProtocol } from 'vscode-debugprotocol'; -import type { BrightScriptDebugCompileError } from '../CompileErrorProcessor'; +import type { BSDebugDiagnostic } from '../CompileErrorProcessor'; import type { LaunchConfiguration } from '../LaunchConfiguration'; import type { ChanperfData } from '../ChanperfTracker'; import type { RendezvousHistory } from '../RendezvousTracker'; -export class CompileFailureEvent implements DebugProtocol.Event { - constructor(compileError: BrightScriptDebugCompileError[]) { - this.body = compileError; +export class CustomEvent implements DebugProtocol.Event { + public constructor(body: T) { + this.body = body; + this.event = this.constructor.name; } - - public body: any; + /** + * The body (payload) of the event. + */ + public body: T; + /** + * The name of the event. This name is how the client identifies the type of event and how to handle it + */ public event: string; + /** + * The type of ProtocolMessage. Hardcoded to 'event' for all custom events + */ + public type = 'event'; public seq: number; - public type: string; } -export class LogOutputEvent implements DebugProtocol.Event { - constructor(lines: string) { - this.body = lines; - this.event = 'BSLogOutputEvent'; +/** + * Emitted when compile errors were encountered during the current debug session, + * usually during the initial sideload process as the Roku is compiling the app. + */ +export class DiagnosticsEvent extends CustomEvent<{ diagnostics: BSDebugDiagnostic[] }> { + constructor(diagnostics: BSDebugDiagnostic[]) { + super({ diagnostics }); } +} - public body: any; - public event: string; - public seq: number; - public type: string; +/** + * Is the object a `DiagnosticsEvent` + */ +export function isDiagnosticsEvent(event: any): event is DiagnosticsEvent { + return !!event && event.event === DiagnosticsEvent.name; +} + +/** + * A line of log ouptut from the Roku device + */ +export class LogOutputEvent extends CustomEvent<{ line: string }> { + constructor(line: string) { + super({ line }); + } } -export class DebugServerLogOutputEvent extends LogOutputEvent { - constructor(lines: string) { - super(lines); - this.event = 'BSDebugServerLogOutputEvent'; +/** + * Is the object a `LogOutputEvent` + */ +export function isLogOutputEvent(event: any): event is LogOutputEvent { + return !!event && event.event === LogOutputEvent.name; +} + +/** + * Log output from the debug server. These are logs emitted from NodeJS from the various RokuCommunity tools + */ +export class DebugServerLogOutputEvent extends CustomEvent<{ line: string }> { + constructor(line: string) { + super({ line }); } } -export class RendezvousEvent implements DebugProtocol.Event { +/** + * Is the object a `DebugServerLogOutputEvent` + */ +export function isDebugServerLogOutputEvent(event: any): event is DebugServerLogOutputEvent { + return !!event && event.event === DebugServerLogOutputEvent.name; +} + +/** + * Emitted when a rendezvous has occurred. Contains the full history of rendezvous since the start of the current debug session + */ +export class RendezvousEvent extends CustomEvent { constructor(output: RendezvousHistory) { - this.body = output; - this.event = 'BSRendezvousEvent'; + super(output); } +} - public body: RendezvousHistory; - public event: string; - public seq: number; - public type: string; +/** + * Is the object a `RendezvousEvent` + */ +export function isRendezvousEvent(event: any): event is RendezvousEvent { + return !!event && event.event === RendezvousEvent.name; } -export class ChanperfEvent implements DebugProtocol.Event { +/** + * Emitted anytime the debug session receives chanperf data. + */ +export class ChanperfEvent extends CustomEvent { constructor(output: ChanperfData) { - this.body = output; - this.event = 'BSChanperfEvent'; + super(output); } +} - public body: ChanperfData; - public event: string; - public seq: number; - public type: string; +/** + * Is the object a `ChanperfEvent` + */ +export function isChanperfEvent(event: any): event is ChanperfEvent { + return !!event && event.event === ChanperfEvent.name; } -export class LaunchStartEvent implements DebugProtocol.Event { - constructor(args: LaunchConfiguration) { - this.body = args; - this.event = 'BSLaunchStartEvent'; + +/** + * Emitted when the launch sequence first starts. This is right after the debug session receives the `launch` request, + * which happens before any zipping, sideloading, etc. + */ +export class LaunchStartEvent extends CustomEvent { + constructor(launchConfiguration: LaunchConfiguration) { + super(launchConfiguration); } +} - public body: any; - public event: string; - public seq: number; - public type: string; +/** + * Is the object a `LaunchStartEvent` + */ +export function isLaunchStartEvent(event: any): event is LaunchStartEvent { + return !!event && event.event === LaunchStartEvent.name; +} + +/** + * This event indicates that the client should show a popup message with the supplied information + */ +export class PopupMessageEvent extends CustomEvent<{ message: string; severity: 'error' | 'info' | 'warn'; modal: boolean }> { + constructor(message: string, severity: 'error' | 'info' | 'warn', modal = false) { + super({ message, severity, modal }); + } +} + +/** + * Is the object a `PopupMessageEvent` + */ +export function isPopupMessageEvent(event: any): event is PopupMessageEvent { + return !!event && event.event === PopupMessageEvent.name; +} + +/** + * Emitted once the channel has been sideloaded to the channel and the session is ready to start actually debugging. + */ +export class ChannelPublishedEvent extends CustomEvent<{ launchConfiguration: LaunchConfiguration }> { + constructor( + launchConfiguration: LaunchConfiguration + ) { + super({ launchConfiguration }); + } +} + +/** + * Is the object a `ChannelPublishedEvent` + */ +export function isChannelPublishedEvent(event: any): event is ChannelPublishedEvent { + return !!event && event.event === ChannelPublishedEvent.name; +} + +/** + * Event that asks the client to execute a command. + */ +export class CustomRequestEvent extends CustomEvent { + constructor(body: R) { + super(body); + } +} + +/** + * Is the object a `CustomRequestEvent` + */ +export function isCustomRequestEvent(event: any): event is CustomRequestEvent { + return !!event && event.event === CustomRequestEvent.name; +} + +export function isExecuteTaskCustomRequest(event: any): event is CustomRequestEvent<{ task: string }> { + return !!event && event.event === CustomRequestEvent.name && event.body.name === 'executeTask'; +} + +export enum ClientToServerCustomEventName { + customRequestEventResponse = 'customRequestEventResponse' } export enum StoppedEventReason { @@ -75,5 +187,9 @@ export enum StoppedEventReason { breakpoint = 'breakpoint', exception = 'exception', pause = 'pause', - entry = 'entry' + entry = 'entry', + goto = 'goto', + functionBreakpoint = 'function breakpoint', + dataBreakpoint = 'data breakpoint', + instructionBreakpoint = 'instruction breakpoint' } diff --git a/src/index.ts b/src/index.ts index 8e266d1b..c48abc78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,12 @@ - -//export everything we need 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/interfaces.ts b/src/interfaces.ts index 6ad7666b..7505dca4 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -19,4 +19,8 @@ export interface AdapterOptions { host: string; brightScriptConsolePort?: number; remotePort?: number; + /** + * If true, the application being debugged will stop on the first line of the program. + */ + stopOnEntry?: boolean; } diff --git a/src/logging.spec.ts b/src/logging.spec.ts new file mode 100644 index 00000000..859dff55 --- /dev/null +++ b/src/logging.spec.ts @@ -0,0 +1,248 @@ +import { expect } from 'chai'; +import * as fsExtra from 'fs-extra'; +import * as sinonActual from 'sinon'; +import type { LaunchConfiguration } from './LaunchConfiguration'; +import { standardizePath as s } from 'brighterscript'; +import { FileLoggingManager, fileTransport } from './logging'; + +const sinon = sinonActual.createSandbox(); +const tempDir = s`${__dirname}/../../.tmp`; + +describe('LoggingManager', () => { + let manager: FileLoggingManager; + + beforeEach(() => { + fsExtra.emptydirSync(tempDir); + sinon.restore(); + manager = new FileLoggingManager(); + //register a writer that discards all log output + fileTransport.setWriter(() => { }); + }); + + afterEach(() => { + fsExtra.emptydirSync(tempDir); + sinon.restore(); + }); + + describe('configure', () => { + function configure(config: Partial, cwd?: string) { + manager.activate(config as any, cwd); + } + it('disables when not specified', () => { + configure(undefined); + expect(manager['fileLogging'].rokuDevice.enabled).to.be.false; + expect(manager['fileLogging'].debugger.enabled).to.be.false; + }); + + it('disables when set to disabled', () => { + configure({ + enabled: false + }); + expect(manager['fileLogging'].rokuDevice.enabled).to.be.false; + expect(manager['fileLogging'].debugger.enabled).to.be.false; + }); + + it('enables both when set to true', () => { + configure({ + enabled: true + }); + expect(manager['fileLogging'].rokuDevice.enabled).to.be.true; + expect(manager['fileLogging'].debugger.enabled).to.be.true; + }); + + it('disables one when explicitly disabled', () => { + configure({ + rokuDevice: false + }); + expect(manager['fileLogging'].rokuDevice.enabled).to.be.false; + expect(manager['fileLogging'].debugger.enabled).to.be.true; + + configure({ + debugger: false + }); + expect(manager['fileLogging'].rokuDevice.enabled).to.be.true; + expect(manager['fileLogging'].debugger.enabled).to.be.false; + }); + + it('uses logfile path when specified and mode="append"', () => { + configure({ + rokuDevice: { + filename: 'telnet.log', + mode: 'append' + }, + debugger: { + filename: 'dbg.log', + mode: 'append' + } + }, tempDir); + expect( + manager['fileLogging'].rokuDevice.filePath + ).to.eql( + s`${tempDir}/logs/telnet.log` + ); + expect( + manager['fileLogging'].debugger.filePath + ).to.eql( + s`${tempDir}/logs/dbg.log` + ); + }); + + it('generates a rolling logfile when specified as session', () => { + let dateText = manager['getLogDate'](new Date()); + sinon.stub(manager as any, 'getLogDate').callsFake((...args) => { + return dateText; + }); + configure({ + rokuDevice: true + }, tempDir); + expect( + manager['fileLogging'].rokuDevice.filePath + ).to.eql( + s`${tempDir}/logs/${dateText}-rokuDevice.log` + ); + }); + + it('uses default dir when not specified', () => { + let dateText = manager['getLogDate'](new Date()); + sinon.stub(manager as any, 'getLogDate').callsFake((...args) => { + return dateText; + }); + configure(true, tempDir); + expect( + manager['fileLogging'].rokuDevice.filePath + ).to.eql( + s`${tempDir}/logs/${dateText}-rokuDevice.log` + ); + expect( + manager['fileLogging'].debugger.filePath + ).to.eql( + s`${tempDir}/logs/${dateText}-debugger.log` + ); + }); + + it('uses root-level dir when specified', () => { + let dateText = manager['getLogDate'](new Date()); + sinon.stub(manager as any, 'getLogDate').callsFake((...args) => { + return dateText; + }); + configure({ + dir: s`${tempDir}/logs2` + }, tempDir); + expect( + manager['fileLogging'].rokuDevice.filePath + ).to.eql( + s`${tempDir}/logs2/${dateText}-rokuDevice.log` + ); + expect( + manager['fileLogging'].debugger.filePath + ).to.eql( + s`${tempDir}/logs2/${dateText}-debugger.log` + ); + }); + + it('uses log-type level dir when specified', () => { + let dateText = manager['getLogDate'](new Date()); + sinon.stub(manager as any, 'getLogDate').callsFake((...args) => { + return dateText; + }); + configure({ + rokuDevice: { + dir: s`${tempDir}/one` + }, + debugger: { + dir: s`${tempDir}/two` + } + }, tempDir); + expect( + manager['fileLogging'].rokuDevice.filePath + ).to.eql( + s`${tempDir}/one/${dateText}-rokuDevice.log` + ); + expect( + manager['fileLogging'].debugger.filePath + ).to.eql( + s`${tempDir}/two/${dateText}-debugger.log` + ); + }); + + it('uses log-type level dir when specified', () => { + let dateText = manager['getLogDate'](new Date()); + sinon.stub(manager as any, 'getLogDate').callsFake((...args) => { + return dateText; + }); + configure({ + rokuDevice: { + dir: s`${tempDir}/one` + }, + debugger: { + dir: s`${tempDir}/two` + } + }, tempDir); + expect( + manager['fileLogging'].rokuDevice.filePath + ).to.eql( + s`${tempDir}/one/${dateText}-rokuDevice.log` + ); + expect( + manager['fileLogging'].debugger.filePath + ).to.eql( + s`${tempDir}/two/${dateText}-debugger.log` + ); + }); + }); + + describe('pruneLogDir', () => { + let logsDir = s`${tempDir}/logs`; + beforeEach(() => { + fsExtra.ensureDirSync(logsDir); + }); + + function writeLogs(dir, filename: string, count: number) { + const paths: string[] = []; + let startDate = new Date(); + for (let i = 0; i < count; i++) { + startDate.setSeconds(startDate.getSeconds() + 1); + + paths.push( + s`${dir}/${manager['getLogDate'](startDate)}-${filename}` + ); + fsExtra.writeFileSync(paths[paths.length - 1], ''); + } + return paths; + } + + it('does not crash when no files were found', () => { + expect( + manager['pruneLogDir'](logsDir, 'log.log', 100) + ).to.eql([]); + }); + + it('does not delete matching files when under the max', () => { + const paths = writeLogs(logsDir, 'rokuDevice.log', 5); + expect( + manager['pruneLogDir'](logsDir, 'rokuDevice.log', 10) + //empty array means no files were deleted + ).to.eql([]); + }); + + it('prunes the oldest files when over the max', () => { + const paths = writeLogs(logsDir, 'rokuDevice.log', 5); + expect( + manager['pruneLogDir'](logsDir, 'rokuDevice.log', 2) + ).to.eql([ + paths[0], + paths[1] + ]); + expect(fsExtra.pathExistsSync(paths[0])).to.be.false; + expect(fsExtra.pathExistsSync(paths[1])).to.be.false; + }); + + it('does not prune when having exactly max number', () => { + const paths = writeLogs(logsDir, 'rokuDevice.log', 5); + expect( + manager['pruneLogDir'](logsDir, 'rokuDevice.log', 5) + //empty array means no files were deleted + ).to.eql([]); + }); + }); +}); diff --git a/src/logging.ts b/src/logging.ts index c5614196..4bb0f426 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,7 +1,15 @@ import { default as defaultLogger } from '@rokucommunity/logger'; import type { Logger } from '@rokucommunity/logger'; import { QueuedTransport } from '@rokucommunity/logger/dist/transports/QueuedTransport'; -const logger = defaultLogger.createLogger('[roku-debug]'); +import { FileTransport } from '@rokucommunity/logger/dist/transports/FileTransport'; +import type { LaunchConfiguration } from './LaunchConfiguration'; +import * as path from 'path'; +import { util } from './util'; +import * as fsExtra from 'fs-extra'; +import * as dateformat from 'dateformat'; +import { standardizePath as s } from './FileUtils'; + +const logger = defaultLogger.createLogger('[dap]'); //disable colors logger.enableColor = false; @@ -9,11 +17,149 @@ logger.enableColor = false; logger.consistentLogLevelWidth = true; export const debugServerLogOutputEventTransport = new QueuedTransport(); +/** + * A transport for logging that allows us to write all log output to a file. This should only be activated if the user has enabled 'debugger' file logging + */ +export const fileTransport = new FileTransport(); //add transport immediately so we can queue log entries logger.addTransport(debugServerLogOutputEventTransport); +logger.addTransport(fileTransport); logger.logLevel = 'log'; const createLogger = logger.createLogger.bind(logger) as typeof Logger.prototype.createLogger; export { logger, createLogger }; export type { Logger, LogMessage, LogLevel } from '@rokucommunity/logger'; +export { LogLevelPriority } from '@rokucommunity/logger'; + +export class FileLoggingManager { + + private fileLogging = { + rokuDevice: { + enabled: false, + filePath: undefined as string + }, + debugger: { + enabled: false, + filePath: undefined as string + } + }; + + /** + * Activate this manager and start processing log data + */ + public activate(config: LaunchConfiguration['fileLogging'], cwd: string) { + cwd ??= process.cwd(); + this.fileLogging = { + rokuDevice: { + enabled: false, + filePath: undefined + }, + debugger: { + enabled: false, + filePath: undefined + } + }; + //diisable all file logging if top-level config is omitted or set to false + if (!config || (typeof config === 'object' && config?.enabled === false)) { + return; + } + let fileLogging = typeof config === 'object' ? { ...config } : {}; + + let defaultDir = path.resolve( + cwd, + fileLogging.dir ?? './logs' + ); + let defaultLogLimit = fileLogging.logLimit ?? Number.MAX_SAFE_INTEGER; + + for (const logType of ['rokuDevice', 'debugger'] as Array<'rokuDevice' | 'debugger'>) { + //rokuDevice log stuff + if (util.isNullish(fileLogging[logType]) || fileLogging[logType] === true) { + fileLogging[logType] = { + enabled: true + }; + } + const logObj = fileLogging[logType]; + if (typeof logObj === 'object') { + //enabled unless explicitly disabled + this.fileLogging[logType].enabled = logObj?.enabled === false ? false : true; + const logLimit = logObj.logLimit ?? defaultLogLimit; + let filename = logObj.filename ?? `${logType}.log`; + let mode = logObj.mode ?? 'session'; + const dir = path.resolve( + cwd, + logObj.dir ?? defaultDir + ); + if (mode === 'session') { + filename = `${this.getLogDate(new Date())}-${filename}`; + //discard the excess session logs that match this filename + this.pruneLogDir(dir, filename, logLimit); + } + + this.fileLogging[logType].filePath = path.resolve( + logObj.dir ?? defaultDir, + filename + ); + } + } + + //if debugger logging is enabled, register the file path which will flush the logs and write all future logs + if (this.fileLogging.debugger.enabled) { + fileTransport.setLogFilePath(this.fileLogging.debugger.filePath); + + //debugger logging is disabled. remove the transport so we don't waste memory queueing log data indefinitely + } else { + logger.removeTransport(fileTransport); + } + } + + /** + * Delete excess log files matching the given filename (and preceeding timestamp) + */ + private pruneLogDir(dir: string, filename: string, max: number) { + const regexp = new RegExp(`\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d∶\\d\\d∶\\d\\d-${filename}`, 'i'); + let files: string[] = []; + try { + //get all the files from this dir + files = fsExtra.readdirSync(dir) + //keep only the file paths that match our filename pattern + .filter(x => regexp.test(x)) + .map(x => s`${dir}/${x}`) + //sort alphabetically + .sort(); + } catch { } + + if (files.length > max) { + let filesToDelete = files.splice(0, files.length - max - 1); + for (const file of filesToDelete) { + fsExtra.removeSync(file); + } + return filesToDelete; + //discard the keepers in order to get the list of files to delete + } else { + return []; + } + } + + /** + * Generate a date string used for log filenames + */ + private getLogDate(date: Date) { + return `${dateformat(date, 'yyyy-mm-dd"T"HH∶MM∶ss')}`; + } + + /** + * Write output from telnet/IO port from the roku device to the file log (if enabled). + */ + public writeRokuDeviceLog(logOutput: string) { + try { + if (this.fileLogging.rokuDevice.enabled) { + fsExtra.appendFileSync(this.fileLogging.rokuDevice.filePath, logOutput); + } + } catch (e) { + console.error(e); + } + } +} + +export type FileLoggingType = 'rokuDevice' | 'debugger'; 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 new file mode 100644 index 00000000..94d31f73 --- /dev/null +++ b/src/managers/ActionQueue.ts @@ -0,0 +1,66 @@ +import type { Deferred } from '../util'; +import { defer } from '../util'; + +/** + * A runner that will keep retrying an action until it succeeds, while also queueing up future actions. + * Will run until all pending actions are complete. + */ +export class ActionQueue { + + private queueItems: Array<{ + action: () => boolean | Promise; + deferred: Deferred; + maxTries: number; + tryCount: number; + }> = []; + + /** + * 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(), + 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 { + 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 ac3caa27..258508cf 100644 --- a/src/managers/BreakpointManager.spec.ts +++ b/src/managers/BreakpointManager.spec.ts @@ -1,14 +1,17 @@ import { expect } from 'chai'; import * as fsExtra from 'fs-extra'; import { SourceMapConsumer, SourceNode } from 'source-map'; - +import type { BreakpointWorkItem } from './BreakpointManager'; import { BreakpointManager } from './BreakpointManager'; import { fileUtils, standardizePath as s } from '../FileUtils'; -import { Project } from './ProjectManager'; +import { ComponentLibraryProject, Project, ProjectManager } from './ProjectManager'; let n = fileUtils.standardizePath.bind(fileUtils); import type { SourceLocation } from '../managers/LocationManager'; import { LocationManager } from '../managers/LocationManager'; import { SourceMapManager } from './SourceMapManager'; +import { expectPickEquals, pickArray } from '../testHelpers.spec'; +import { createSandbox } from 'sinon'; +const sinon = createSandbox(); describe('BreakpointManager', () => { let cwd = fileUtils.standardizePath(process.cwd()); @@ -19,14 +22,19 @@ describe('BreakpointManager', () => { let distDir = s`${tmpDir}/dist`; let srcDir = s`${tmpDir}/src`; let outDir = s`${tmpDir}/out`; + const srcPath = s`${rootDir}/source/main.brs`; + const complib1RootDir = s`${tmpDir}/complib1/rootDir`; + const complib1OutDir = s`${tmpDir}/complib1/outDir`; + const complib2RootDir = s`${tmpDir}/complib2/rootDir`; + const complib2OutDir = s`${tmpDir}/complib2/outDir`; let bpManager: BreakpointManager; let locationManager: LocationManager; let sourceMapManager: SourceMapManager; - //cast the manager as any to simplify some of the tests - let b: any; + let projectManager: ProjectManager; + beforeEach(() => { - fsExtra.ensureDirSync(tmpDir); + sinon.restore(); fsExtra.emptyDirSync(tmpDir); fsExtra.ensureDirSync(`${rootDir}/source`); fsExtra.ensureDirSync(`${stagingDir}/source`); @@ -37,19 +45,113 @@ describe('BreakpointManager', () => { sourceMapManager = new SourceMapManager(); locationManager = new LocationManager(sourceMapManager); bpManager = new BreakpointManager(sourceMapManager, locationManager); - b = bpManager; + projectManager = new ProjectManager(bpManager, locationManager); + projectManager.mainProject = new Project({ + rootDir: rootDir, + files: [], + outDir: s`${outDir}/mainProject` + }); + projectManager.addComponentLibraryProject( + new ComponentLibraryProject({ + rootDir: complib1RootDir, + files: [], + libraryIndex: 0, + outDir: complib1OutDir, + outFile: s`${complib1OutDir}/complib1.zip` + }) + ); + projectManager.addComponentLibraryProject( + new ComponentLibraryProject({ + rootDir: complib2RootDir, + files: [], + libraryIndex: 1, + outDir: complib2OutDir, + outFile: s`${complib2OutDir}/complib2.zip` + }) + ); }); afterEach(() => { + sinon.restore(); 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`); }); it('returns the the found key when it already exists', () => { - b.breakpointsByFilePath[s`A/B/C`] = []; + bpManager['breakpointsByFilePath'].set(s`A/B/C`, []); expect(bpManager.sanitizeSourceFilePath('a/b/c')).to.equal(s`A/B/C`); }); }); @@ -177,32 +279,8 @@ describe('BreakpointManager', () => { }); }); - describe('setBreakpointsForFile', () => { - it('verifies all breakpoints before launch', () => { - let breakpoints = bpManager.replaceBreakpoints(n(`${cwd}/file.brs`), [{ - line: 0, - column: 0 - }, { - line: 1, - column: 0 - }]); - expect(breakpoints).to.be.lengthOf(2); - expect(breakpoints[0]).to.include({ - line: 0, - column: 0, - verified: true, - wasAddedBeforeLaunch: true - }); - expect(breakpoints[1]).to.include({ - line: 1, - column: 0, - verified: true, - wasAddedBeforeLaunch: true - }); - }); - + describe('replaceBreakpoints', () => { it('does not verify breakpoints after launch', () => { - bpManager.lockBreakpoints(); let breakpoints = bpManager.replaceBreakpoints(n(`${cwd}/file.brs`), [{ line: 0, column: 0 @@ -211,50 +289,67 @@ describe('BreakpointManager', () => { expect(breakpoints[0]).to.deep.include({ line: 0, column: 0, - verified: false, - wasAddedBeforeLaunch: false + verified: false }); }); - it('re-verifies breakpoint after launch toggle', () => { + it('re-verifies breakpoint after launch toggle', async () => { //set the breakpoint before launch - let breakpoints = bpManager.replaceBreakpoints(s`${cwd}/file.brs`, [{ + let breakpoints = bpManager.replaceBreakpoints(s`${rootDir}/file.brs`, [{ line: 2 }]); expect(breakpoints).to.be.lengthOf(1); expect(breakpoints[0]).to.deep.include({ line: 2, column: 0, - verified: true, - isHidden: false + verified: false }); - //launch - bpManager.lockBreakpoints(); + //write the breakpoints to the files + await projectManager.breakpointManager.writeBreakpointsForProject(projectManager.mainProject); - //simulate user deleting all breakpoints - breakpoints = bpManager.replaceBreakpoints(s`${cwd}/file.brs`, []); - - expect(breakpoints).to.be.lengthOf(1); expect(breakpoints[0]).to.deep.include({ line: 2, - verified: true, - isHidden: true + column: 0, + verified: true }); + //simulate user deleting all breakpoints + breakpoints = bpManager.replaceBreakpoints(s`${rootDir}/file.brs`, []); + + expect(breakpoints).to.be.lengthOf(0); + //simulate user adding a breakpoint to the same place it had been before - breakpoints = bpManager.replaceBreakpoints(s`${cwd}/file.brs`, [{ + breakpoints = bpManager.replaceBreakpoints(s`${rootDir}/file.brs`, [{ line: 2 }]); expect(breakpoints).to.be.lengthOf(1); expect(breakpoints[0]).to.deep.include({ line: 2, column: 0, - verified: true, - wasAddedBeforeLaunch: true, - isHidden: false + verified: true }); }); + + it('retains breakpoint data for breakpoints that did not change', () => { + //set the breakpoint before launch + let breakpoints = bpManager.replaceBreakpoints(s`${cwd}/file.brs`, [{ + line: 2 + }, { + line: 4, + condition: 'true' + }]).map(x => ({ ...x })); + + const replacedBreakpoints = bpManager.replaceBreakpoints(s`${cwd}/file.brs`, [{ + line: 2 + }, { + line: 4, + condition: 'true' + }]).map(x => ({ ...x })); + + //the breakpoints should be identical + expect(breakpoints).to.eql(replacedBreakpoints); + }); }); describe('writeBreakpointsForProject', () => { @@ -289,9 +384,6 @@ describe('BreakpointManager', () => { //copy the file to staging fsExtra.copyFileSync(`${rootDir}/source/main.brs`, `${stagingDir}/source/main.brs`); - //launch - bpManager.lockBreakpoints(); - //file was copied to staging expect(fsExtra.pathExistsSync(`${stagingDir}/source/main.brs`)).to.be.true; //sourcemap was not yet created @@ -300,7 +392,7 @@ describe('BreakpointManager', () => { await bpManager.writeBreakpointsForProject(new Project({ rootDir: rootDir, outDir: outDir, - stagingFolderPath: stagingDir + stagingDir: stagingDir })); //it wrote the breakpoint in the correct location @@ -333,9 +425,6 @@ describe('BreakpointManager', () => { column: 0 }]); - //launch - bpManager.lockBreakpoints(); - //sourcemap was not yet created expect(fsExtra.pathExistsSync(`${stagingDir}/source/main.brs.map`)).to.be.false; @@ -344,7 +433,7 @@ describe('BreakpointManager', () => { rootDir: rootDir, outDir: s`${cwd}/out`, sourceDirs: [sourceDir1], - stagingFolderPath: stagingDir + stagingDir: stagingDir }) ); @@ -377,15 +466,12 @@ describe('BreakpointManager', () => { column: 0 }]); - //launch - bpManager.lockBreakpoints(); - await bpManager.writeBreakpointsForProject( new Project({ rootDir: rootDir, outDir: s`${cwd}/out`, sourceDirs: [sourceDir1, sourceDir2], - stagingFolderPath: stagingDir + stagingDir: stagingDir }) ); @@ -423,15 +509,12 @@ describe('BreakpointManager', () => { hitCondition: '3' }]); - //launch - bpManager.lockBreakpoints(); - await bpManager.writeBreakpointsForProject( new Project({ rootDir: rootDir, outDir: s`${cwd}/out`, sourceDirs: [sourceDir1, sourceDir2], - stagingFolderPath: stagingDir + stagingDir: stagingDir }) ); @@ -468,15 +551,12 @@ describe('BreakpointManager', () => { column: 0 }]); - //launch - bpManager.lockBreakpoints(); - await bpManager.writeBreakpointsForProject( new Project({ rootDir: rootDir, outDir: s`${cwd}/out`, sourceDirs: [sourceDir1, sourceDir2], - stagingFolderPath: stagingDir + stagingDir: stagingDir }) ); @@ -520,10 +600,10 @@ describe('BreakpointManager', () => { fsExtra.writeFileSync(s`${stagingDir}/main.brs.map`, result.map.toString()); //set a few breakpoints in the source files - bpManager.registerBreakpoint(src, { + bpManager.setBreakpoint(src, { line: 5 }); - bpManager.registerBreakpoint(src, { + bpManager.setBreakpoint(src, { line: 7 }); @@ -533,7 +613,7 @@ describe('BreakpointManager', () => { ], rootDir: s`${tmpDir}/dist`, outDir: s`${tmpDir}/out`, - stagingFolderPath: stagingDir + stagingDir: stagingDir })); //the breakpoints should be placed in the proper locations @@ -629,7 +709,7 @@ describe('BreakpointManager', () => { column: 4 }); - bpManager.registerBreakpoint(sourceFilePath, { + bpManager.setBreakpoint(sourceFilePath, { line: 3, column: 0 }); @@ -638,7 +718,7 @@ describe('BreakpointManager', () => { files: [ 'source/main.brs' ], - stagingFolderPath: stagingDir, + stagingDir: stagingDir, outDir: outDir, rootDir: rootDir })); @@ -650,7 +730,7 @@ describe('BreakpointManager', () => { lineNumber: 2, fileMappings: [], rootDir: rootDir, - stagingFolderPath: stagingDir, + stagingDir: stagingDir, enableSourceMaps: true }); @@ -690,7 +770,7 @@ describe('BreakpointManager', () => { ]); //write breakpoints - bpManager.registerBreakpoint(sourceFilePath, { + bpManager.setBreakpoint(sourceFilePath, { line: 4, column: 0 }); @@ -699,7 +779,7 @@ describe('BreakpointManager', () => { files: [ 'source/main.brs' ], - stagingFolderPath: stagingDir, + stagingDir: stagingDir, outDir: outDir, rootDir: rootDir })); @@ -724,7 +804,7 @@ describe('BreakpointManager', () => { `); //write breakpoints - bpManager.registerBreakpoint(baseFilePath, { + bpManager.setBreakpoint(baseFilePath, { line: 2, column: 0 }); @@ -737,7 +817,7 @@ describe('BreakpointManager', () => { dest: '' } ], - stagingFolderPath: stagingDir, + stagingDir: stagingDir, outDir: outDir, rootDir: rootDir }); @@ -750,4 +830,371 @@ describe('BreakpointManager', () => { baseFilePath ]); }); + + it('adds breakpoint keys', () => { + expect( + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2 + }, { + line: 3, + condition: 'true' + }, { + line: 4, + hitCondition: '2' + }, { + line: 5, + column: 12 + }, { + line: 6, + logMessage: 'hello world' + }]).map(x => x.srcHash).sort() + ).to.eql([ + s`${rootDir}/source/main.brs:2:0-standard`, + s`${rootDir}/source/main.brs:3:0-condition=true`, + s`${rootDir}/source/main.brs:4:0-hitCondition=2`, + s`${rootDir}/source/main.brs:5:12-standard`, + s`${rootDir}/source/main.brs:6:0-logMessage=hello world` + ].sort()); + }); + + it('does not duplicate breakpoints that have the same key', () => { + const pkgPath = s`${rootDir}/source/main.brs`; + bpManager.setBreakpoint(pkgPath, { + line: 2 + }); + bpManager.setBreakpoint(pkgPath, { + line: 2 + }); + expect( + bpManager['getBreakpointsForFile'](pkgPath).map(x => x.srcHash) + ).to.eql([ + s`${pkgPath}:2:0-standard` + ]); + }); + + it('replaces breakpoints with distinct attributes', () => { + const pkgPath = s`${rootDir}/source/main.brs`; + + bpManager.setBreakpoint(pkgPath, { + line: 2 + }); + expect( + bpManager['getBreakpointsForFile'](pkgPath).map(x => x.srcHash) + ).to.eql([ + s`${pkgPath}:2:0-standard` + ]); + + bpManager.setBreakpoint(pkgPath, { + line: 2, + condition: 'true' + }); + expect( + bpManager['getBreakpointsForFile'](pkgPath).map(x => x.srcHash) + ).to.eql([ + s`${pkgPath}:2:0-condition=true` + ]); + + bpManager.setBreakpoint(pkgPath, { + line: 2, + hitCondition: '4' + }); + expect( + bpManager['getBreakpointsForFile'](pkgPath).map(x => x.srcHash) + ).to.eql([ + s`${pkgPath}:2:0-hitCondition=4` + ]); + }); + + it('keeps breakpoints verified if they did not change', () => { + let breakpoints = bpManager.replaceBreakpoints(srcPath, [{ + line: 10 + }]); + //mark this breakpoint as verified + breakpoints[0].verified = true; + + breakpoints = bpManager.replaceBreakpoints(srcPath, [{ + line: 10 + }, { + line: 11 + }]); + + expectPickEquals(breakpoints, [{ + line: 10, + verified: true + }, { + line: 11, + verified: false + }]); + }); + + describe('getDiff', () => { + async function testDiffEquals( + expected?: { + added?: Array>; + removed?: Array>; + unchanged?: Array>; + }, + projects = [projectManager.mainProject, ...projectManager.componentLibraryProjects] + ) { + const diff = await bpManager.getDiff(projects); + //filter the result by the list of properties from each test value + expected = { + added: [], + removed: [], + unchanged: [], + ...expected ?? {} + }; + const actual = { + added: pickArray(diff.added, expected.added), + removed: pickArray(diff.removed, expected.removed), + unchanged: pickArray(diff.unchanged, expected.unchanged) + }; + + expect(actual).to.eql(expected); + + return diff; + } + + it('returns empty diff when no projects are present', async () => { + await testDiffEquals({ added: [], removed: [], unchanged: [] }, []); + }); + + it('returns empty diff when no breakpoints are registered', async () => { + await testDiffEquals({ added: [], removed: [], unchanged: [] }); + }); + + it('recovers from invalid sourceDirs', async () => { + bpManager.launchConfiguration = { + ...(bpManager?.launchConfiguration ?? {} as any), + sourceDirs: ['source/**/*', 'components/**/*'] + }; + + bpManager.replaceBreakpoints(`${rootDir}/components/tasks/baseTask.brs`, [{ line: 2 }]); + bpManager.replaceBreakpoints(`${rootDir}/source/main.brs`, [{ line: 2 }]); + let diff = await testDiffEquals({ + added: [{ + pkgPath: 'pkg:/components/tasks/baseTask.brs', + line: 2 + }, { + pkgPath: 'pkg:/source/main.brs', + line: 2 + }] + }); + + //set the deviceId for the breakpoints + bpManager.setBreakpointDeviceId(diff.added[0].srcHash, diff.added[0].destHash, 1); + bpManager.setBreakpointDeviceId(diff.added[1].srcHash, diff.added[1].destHash, 2); + + //mark the breakpoints as verified + bpManager.verifyBreakpoint(1, true); + bpManager.verifyBreakpoint(2, true); + + //call the getDiff a few more times + await testDiffEquals({ + unchanged: [{ + pkgPath: 'pkg:/components/tasks/baseTask.brs', + line: 2 + }, { + pkgPath: 'pkg:/source/main.brs', + line: 2 + }] + }); + await testDiffEquals({ + unchanged: [{ + pkgPath: 'pkg:/components/tasks/baseTask.brs', + line: 2 + }, { + pkgPath: 'pkg:/source/main.brs', + line: 2 + }] + }); + + //add another breakpoint to the list + bpManager.replaceBreakpoints(`${rootDir}/source/main.brs`, [{ line: 2 }, { line: 7 }]); + + await testDiffEquals({ + added: [{ + pkgPath: 'pkg:/source/main.brs', + line: 7 + }], + unchanged: [{ + pkgPath: 'pkg:/components/tasks/baseTask.brs', + line: 2 + }, { + pkgPath: 'pkg:/source/main.brs', + line: 2 + }] + }); + }); + + + it('handles breakpoint flow', async () => { + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2 + }]); + //breakpoint should show up first time + await testDiffEquals({ + added: [{ + pkgPath: 'pkg:/source/main.brs', + line: 2 + }] + }); + + //should show as "unchanged" now + await testDiffEquals({ + unchanged: [{ + pkgPath: 'pkg:/source/main.brs', + line: 2 + }] + }); + + //remove the breakpoint + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, []); + + //breakpoint should be in the "removed" bucket + await testDiffEquals({ + removed: [{ + pkgPath: 'pkg:/source/main.brs', + line: 2 + }] + }); + + //there should be no breakpoint changes + await testDiffEquals(); + }); + + it('detects hitCount change', async () => { + //add breakpoint with hit condition + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2, + hitCondition: '2' + }]); + + await testDiffEquals({ + added: [{ + line: 2, + hitCondition: '2' + }] + }); + + //change the breakpoint hit condition + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2, + hitCondition: '1' + }]); + + await testDiffEquals({ + removed: [{ + line: 2, + hitCondition: '2' + }], + added: [{ + line: 2, + hitCondition: '1' + }] + }); + }); + + it('detects column number change (roku does not support this yet, but we might as well...)', async () => { + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2, + column: 4 + }]); + + await testDiffEquals({ + added: [{ + line: 2, + column: 4 + }] + }); + + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2, + column: 8 + }]); + + await testDiffEquals({ + removed: [{ + line: 2, + column: 4 + }], + added: [{ + line: 2, + column: 8 + }] + }); + }); + + it('maintains breakpoint IDs', async () => { + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2, + column: 4 + }]); + + await testDiffEquals({ + added: [{ + line: 2, + column: 4 + }] + }); + + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 2, + column: 8 + }]); + + await testDiffEquals({ + removed: [{ + line: 2, + column: 4 + }], + added: [{ + line: 2, + column: 8 + }] + }); + }); + + it('accounts for complib filename postfixes', async () => { + bpManager.replaceBreakpoints(s`${rootDir}/source/main.brs`, [{ + line: 1 + }]); + + bpManager.replaceBreakpoints(s`${complib1RootDir}/source/main.brs`, [{ + line: 2 + }]); + + bpManager.replaceBreakpoints(s`${complib2RootDir}/source/main.brs`, [{ + line: 3 + }]); + + await testDiffEquals({ + added: [{ + line: 1, + pkgPath: 'pkg:/source/main.brs' + }, { + line: 2, + pkgPath: 'pkg:/source/main__lib0.brs' + }, { + line: 3, + pkgPath: 'pkg:/source/main__lib1.brs' + }], removed: [], unchanged: [] + }); + }); + + 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 6a6e80ff..093f2f0c 100644 --- a/src/managers/BreakpointManager.ts +++ b/src/managers/BreakpointManager.ts @@ -3,12 +3,14 @@ import { orderBy } from 'natural-orderby'; import type { CodeWithSourceMap } from 'source-map'; import { SourceNode } from 'source-map'; import type { DebugProtocol } from 'vscode-debugprotocol'; -import { fileUtils } from '../FileUtils'; -import type { Project } from './ProjectManager'; +import { fileUtils, standardizePath } from '../FileUtils'; +import type { ComponentLibraryProject, Project } from './ProjectManager'; import { standardizePath as s } from 'roku-deploy'; import type { SourceMapManager } from './SourceMapManager'; import type { LocationManager } from './LocationManager'; import { util } from '../util'; +import { EventEmitter } from 'eventemitter3'; +import { logger, Logger } from '../logging'; export class BreakpointManager { @@ -19,25 +21,44 @@ export class BreakpointManager { } + private logger = logger.createLogger('[bpManager]'); + public launchConfiguration: { sourceDirs: string[]; rootDir: string; enableSourceMaps?: boolean; }; + private emitter = new EventEmitter(); + + private emit(eventName: 'breakpoints-verified', data: { breakpoints: AugmentedSourceBreakpoint[] }); + private emit(eventName: string, data: any) { + this.emitter.emit(eventName, data); + } + /** - * Tell the breakpoint manager that no new breakpoints can be verified - * (most likely due to the app being launched and roku not supporting dynamic breakpoints) + * Subscribe to an event */ - public lockBreakpoints() { - this.areBreakpointsLocked = true; + public on(eventName: 'breakpoints-verified', handler: (data: { breakpoints: AugmentedSourceBreakpoint[] }) => any); + public on(eventName: string, handler: (data: any) => any) { + this.emitter.on(eventName, handler); + return () => { + this.emitter.off(eventName, handler); + }; } /** - * Indicates whether the app has been launched or not. - * This will determine whether the breakpoints should be written to the files, or marked as not verified (greyed out in vscode) + * Get a promise that resolves the next time the specified event occurs */ - private areBreakpointsLocked = false; + 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. @@ -45,107 +66,304 @@ export class BreakpointManager { * These breakpoints are all set before launch, and then this list is not changed again after that. * (this concept may need to be modified once we get live breakpoint support) */ - private breakpointsByFilePath = {} as Record; + private breakpointsByFilePath = new Map(); - public static breakpointIdSequence = 1; + /** + * 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 + */ + private breakpointIdSequence = 1; /** * breakpoint lines are 1-based, and columns are zero-based */ - public registerBreakpoint(sourceFilePath: string, breakpoint: AugmentedSourceBreakpoint | DebugProtocol.SourceBreakpoint) { - sourceFilePath = this.sanitizeSourceFilePath(sourceFilePath); - //get the breakpoints array (and optionally initialize it if not set) - let breakpointsArray = this.breakpointsByFilePath[sourceFilePath] ?? []; - this.breakpointsByFilePath[sourceFilePath] = breakpointsArray; + public setBreakpoint(srcPath: string, breakpoint: AugmentedSourceBreakpoint | DebugProtocol.SourceBreakpoint) { + this.logger.debug('setBreakpoint', { srcPath, breakpoint }); - let existingBreakpoint = breakpointsArray.find(x => x.line === breakpoint.line); + srcPath = this.sanitizeSourceFilePath(srcPath); - let bp = Object.assign(existingBreakpoint || {}, breakpoint); + this.logger.debug('[setBreakpoint] sanitized srcPath', srcPath); - //set column=0 if the breakpoint is missing that field - bp.column = bp.column ?? 0; + //if a breakpoint gets set in rootDir, and we have sourceDirs, convert the rootDir path to sourceDirs path + //so the breakpoint gets moved into the source file instead of the output file + if (this.launchConfiguration?.sourceDirs?.length > 0) { + let lastWorkingPath: string; + for (const sourceDir of this.launchConfiguration.sourceDirs) { + srcPath = srcPath.replace(this.launchConfiguration.rootDir, sourceDir); + if (fsExtra.pathExistsSync(srcPath)) { + lastWorkingPath = srcPath; + } + } + //replace srcPath with the highest sourceDir path that exists + if (lastWorkingPath) { + srcPath = this.sanitizeSourceFilePath(lastWorkingPath); + } + } - bp.wasAddedBeforeLaunch = bp.wasAddedBeforeLaunch ?? this.areBreakpointsLocked === false; + //get the breakpoints array (and optionally initialize it if not set) + let breakpointsArray = this.getBreakpointsForFile(srcPath, true); + + //only a single breakpoint can be defined per line. So, if we find one on this line, we'll augment that breakpoint rather than builiding a new one + const existingBreakpoint = breakpointsArray.find(x => x.line === breakpoint.line); + + this.logger.debug('existingBreakpoint', existingBreakpoint); + + let bp = Object.assign(existingBreakpoint ?? {}, { + //remove common attributes from any existing breakpoint so we don't end up with more info than we need + ...{ + //default to 0 if the breakpoint is missing `column` + column: 0, + condition: undefined, + hitCondition: undefined, + logMessage: undefined + }, + ...breakpoint, + srcPath: srcPath, + //assign a hash-like key to this breakpoint (so we can match against other similar breakpoints in the future) + srcHash: this.getBreakpointSrcHash(srcPath, breakpoint) + } as AugmentedSourceBreakpoint); + + //generate a new id for this breakpoint if one does not exist + bp.id ??= this.breakpointIdSequence++; + + //all breakpoints default to false if not already set to true + bp.verified ??= false; + + //if the breakpoint hash changed, mark the breakpoint as unverified + if (existingBreakpoint?.srcHash !== bp.srcHash) { + bp.verified = false; + } - //set an id if one does not already exist (used for pushing breakpoints to the client) - bp.id = bp.id ?? BreakpointManager.breakpointIdSequence++; + //if this is a new breakpoint, add it to the list. (otherwise, the existing breakpoint is edited in-place) + if (!existingBreakpoint) { + breakpointsArray.push(bp); + } - //any breakpoint set in this function is not hidden - bp.isHidden = false; + //if this is one of the permanent breakpoints, mark it as verified immediately (only applicable to telnet sessions) + if (this.getPermanentBreakpoint(bp.srcHash)) { + this.setBreakpointDeviceId(bp.srcHash, bp.srcHash, bp.id); + this.verifyBreakpoint(bp.id, true); + } - //mark non-supported breakpoint as NOT verified, since we don't support debugging non-brightscript files - if (!fileUtils.hasAnyExtension(sourceFilePath, ['.brs', '.bs', '.xml'])) { - bp.verified = false; + this.logger.debug('setBreakpoint done', bp); - //debug session is not launched yet, all of these breakpoints are treated as verified - } else if (this.areBreakpointsLocked === false) { - //confirm that breakpoint is at a valid location. TODO figure out how to determine valid locations... - bp.verified = true; + return bp; + } - //a debug session is currently running - } else { - //TODO use the standard reverse-lookup logic for converting the rootDir or stagingDir paths into sourceDirs - - //if a breakpoint gets set in rootDir, and we have sourceDirs, convert the rootDir path to sourceDirs path - //so the breakpoint gets moved into the source file instead of the output file - if (this.launchConfiguration?.sourceDirs && this.launchConfiguration.sourceDirs.length > 0) { - let lastWorkingPath = ''; - for (const sourceDir of this.launchConfiguration.sourceDirs) { - sourceFilePath = sourceFilePath.replace(this.launchConfiguration.rootDir, sourceDir); - if (fsExtra.pathExistsSync(sourceFilePath)) { - lastWorkingPath = sourceFilePath; - } - } - sourceFilePath = lastWorkingPath; + /** + * Delete a breakpoint + */ + 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]) + ]); + } - } - //new breakpoints will be verified=false, but breakpoints that were removed and then added again should be verified=true - if (breakpointsArray.find(x => x.wasAddedBeforeLaunch && x.line === bp.line)) { - bp.verified = true; - bp.wasAddedBeforeLaunch = true; - } else { - bp.verified = false; - bp.wasAddedBeforeLaunch = false; + /** + * Delete a set of breakpoints + */ + public deleteBreakpoints(args: BreakpointRef[]) { + this.logger.debug('deleteBreakpoints', args); + + 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]); } } + } - //if we already have a breakpoint for this exact line, don't add another one - if (breakpointsArray.find(x => x.line === breakpoint.line)) { - + /** + * 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 { - //add the breakpoint to the list - breakpointsArray.push(bp); + ref = args[0]; } + return this.getBreakpoints([ref])[0]; } /** - * Set/replace/delete the list of breakpoints for this file. - * @param sourceFilePath - * @param allBreakpointsForFile + * Given a breakpoint ref, turn it into a hash + */ + private refToHash(ref: BreakpointRef): string { + if (!ref) { + return; + } + //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 replaceBreakpoints(sourceFilePath: string, allBreakpointsForFile: DebugProtocol.SourceBreakpoint[]): AugmentedSourceBreakpoint[] { - sourceFilePath = this.sanitizeSourceFilePath(sourceFilePath); + 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); + }); + } - if (this.areBreakpointsLocked) { - //keep verified breakpoints, but toss the rest - this.breakpointsByFilePath[sourceFilePath] = this.getBreakpointsForFile(sourceFilePath) - .filter(x => x.verified); + private deviceIdByDestHash = new Map(); - //hide all of the breakpoints (the active ones will be reenabled later in this method) - for (let bp of this.breakpointsByFilePath[sourceFilePath]) { - bp.isHidden = true; - } + /** + * 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(srcHash: string, destHast: string, deviceId: number) { + this.logger.debug('setBreakpointDeviceId', { srcHash, destHast, deviceId }); + this.deviceIdByDestHash.set(destHast, { srcHash: srcHash, deviceId: deviceId }); + } + + /** + * Mark this breakpoint as verified + */ + public verifyBreakpoint(deviceId: number, isVerified = true) { + const breakpoint = this.getBreakpointByDeviceId(deviceId); + if (breakpoint) { + breakpoint.verified = isVerified; + + this.queueEvent('breakpoints-verified', breakpoint.srcHash); + return true; } else { - //we're not debugging erase all of the breakpoints - this.breakpointsByFilePath[sourceFilePath] = []; + //couldn't find the breakpoint. return false so the caller can handle that properly + return false; + } + } + + private queueEventStates = new Map(); + + + /** + * 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 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(() => { + state.isQueued = false; + const breakpoints = this.getBreakpoints(state.pendingRefs); + state.pendingRefs = []; + this.emit(event as Parameters[0], { + breakpoints: breakpoints + }); + }); } + } - for (let breakpoint of allBreakpointsForFile) { - this.registerBreakpoint(sourceFilePath, breakpoint); + /** + * 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 getBreakpointSrcHash(filePath: string, breakpoint: DebugProtocol.SourceBreakpoint | AugmentedSourceBreakpoint) { + const key = `${standardizePath(filePath)}:${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`; + } + + /** + * 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 + * @param allBreakpointsForFile + */ + public replaceBreakpoints(srcPath: string, allBreakpointsForFile: DebugProtocol.SourceBreakpoint[]): AugmentedSourceBreakpoint[] { + srcPath = this.sanitizeSourceFilePath(srcPath); + + const currentBreakpoints = allBreakpointsForFile.map(breakpoint => this.setBreakpoint(srcPath, breakpoint)); + + //delete all breakpoints from the file that are not currently in this list + this.breakpointsByFilePath.set( + srcPath, + this.getBreakpointsForFile(srcPath).filter(x => currentBreakpoints.includes(x)) + ); + //get the final list of breakpoints - return this.getBreakpointsForFile(sourceFilePath); + return currentBreakpoints; } /** @@ -156,9 +374,7 @@ export class BreakpointManager { let result = {} as Record>; //iterate over every file that contains breakpoints - for (let sourceFilePath in this.breakpointsByFilePath) { - let breakpoints = this.breakpointsByFilePath[sourceFilePath]; - + for (let [sourceFilePath, breakpoints] of this.breakpointsByFilePath) { for (let breakpoint of breakpoints) { //get the list of locations in staging that this breakpoint should be written to. //if none are found, then this breakpoint is ignored @@ -167,10 +383,10 @@ export class BreakpointManager { breakpoint.line, breakpoint.column, [ - ...project.sourceDirs, + ...project?.sourceDirs ?? [], project.rootDir ], - project.stagingFolderPath, + project.stagingDir, project.fileMappings ); @@ -178,21 +394,39 @@ export class BreakpointManager { let relativeStagingPath = fileUtils.replaceCaseInsensitive( stagingLocation.filePath, fileUtils.standardizePath( - fileUtils.removeTrailingSlash(project.stagingFolderPath) + '/' + fileUtils.removeTrailingSlash(project.stagingDir) + '/' ), '' ); + const pkgPath = 'pkg:/' + fileUtils + //replace staging folder path with nothing (so we can build a pkg path) + .replaceCaseInsensitive( + s`${stagingLocation.filePath}`, + s`${project.stagingDir}`, + '' + ) + //force to unix path separators + .replace(/[\/\\]+/g, '/') + //remove leading slash + .replace(/^\//, ''); + let obj: BreakpointWorkItem = { //add the breakpoint info ...breakpoint, //add additional info - sourceFilePath: sourceFilePath, + srcPath: sourceFilePath, + destHash: undefined, rootDirFilePath: s`${project.rootDir}/${relativeStagingPath}`, line: stagingLocation.lineNumber, column: stagingLocation.columnIndex, stagingFilePath: stagingLocation.filePath, - type: stagingLocationsResult.type + type: stagingLocationsResult.type, + 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] = []; } @@ -233,12 +467,37 @@ export class BreakpointManager { let promises = [] as Promise[]; for (let stagingFilePath in breakpointsByStagingFilePath) { - promises.push(this.writeBreakpointsToFile(stagingFilePath, breakpointsByStagingFilePath[stagingFilePath])); + const breakpoints = breakpointsByStagingFilePath[stagingFilePath]; + promises.push(this.writeBreakpointsToFile(stagingFilePath, breakpoints)); + for (const breakpoint of breakpoints) { + //mark this breakpoint as verified + 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); + } } await Promise.all(promises); + + //sort all permanent breakpoints by line and column + for (const [key, breakpoints] of this.permanentBreakpointsBySrcPath) { + this.permanentBreakpointsBySrcPath.set(key, orderBy(breakpoints, [x => x.line, x => x.column])); + } + } + + private registerPermanentBreakpoint(breakpoint: BreakpointWorkItem) { + const collection = this.permanentBreakpointsBySrcPath.get(breakpoint.srcPath) ?? []; + //clone the breakpoint so future updates don't mutate it. + collection.push({ ...breakpoint }); + this.permanentBreakpointsBySrcPath.set(breakpoint.srcPath, collection); } + /** + * The list of breakpoints that were permanently written to a file at the start of a debug session. Used for line offset calculations. + */ + private permanentBreakpointsBySrcPath = new Map(); + /** * Write breakpoints to the specified file, and update the sourcemaps to match */ @@ -257,7 +516,7 @@ export class BreakpointManager { //the calling function will merge this sourcemap into the other existing sourcemap, so just use the same name because it doesn't matter ? breakpoints[0].rootDirFilePath //the calling function doesn't have a sourcemap for this file, so we need to point it to the sourceDirs found location (probably rootDir...) - : breakpoints[0].sourceFilePath; + : breakpoints[0].srcPath; let sourceAndMap = this.getSourceAndMapWithBreakpoints(fileContents, originalFilePath, breakpoints); @@ -332,8 +591,7 @@ export class BreakpointManager { let match: RegExpExecArray; // Get all the value to evaluate as expressions - // eslint-disable-next-line no-cond-assign - while (match = expressionsCheck.exec(logMessage)) { + while ((match = expressionsCheck.exec(logMessage))) { logMessage = logMessage.replace(match[0], `"; ${match[1]};"`); } @@ -380,9 +638,36 @@ export class BreakpointManager { /** * Get the list of breakpoints for the specified file path, or an empty array */ - public getBreakpointsForFile(filePath: string): AugmentedSourceBreakpoint[] { + private getBreakpointsForFile(filePath: string, registerIfMissing = false): AugmentedSourceBreakpoint[] { let key = this.sanitizeSourceFilePath(filePath); - return this.breakpointsByFilePath[key] ?? []; + const result = this.breakpointsByFilePath.get(key) ?? []; + if (registerIfMissing === true) { + this.breakpointsByFilePath.set(key, result); + } + return result; + } + + /** + * Get the permanent breakpoint with the specified hash + * @returns the breakpoint with the matching hash, or undefined + */ + public getPermanentBreakpoint(hash: string) { + for (const [, breakpoints] of this.permanentBreakpointsBySrcPath) { + for (const breakpoint of breakpoints) { + if (breakpoint.srcHash === hash) { + return breakpoint; + } + } + } + } + + /** + * Get the list of breakpoints that were written to the source file + */ + public getPermanentBreakpointsForFile(srcPath: string) { + return this.permanentBreakpointsBySrcPath.get( + this.sanitizeSourceFilePath(srcPath) + ) ?? []; } /** @@ -392,52 +677,256 @@ export class BreakpointManager { public sanitizeSourceFilePath(filePath: string) { filePath = fileUtils.standardizePath(filePath); - for (let key in this.breakpointsByFilePath) { + for (let [key] of this.breakpointsByFilePath) { if (filePath.toLowerCase() === key.toLowerCase()) { return key; } } return filePath; } + + /** + * Determine if there's a breakpoint set at the given staging folder and line. + * This is not trivial, so only run when absolutely necessary + * @param projects the list of projects to scan + * @param pkgPath the path to the file in the staging directory + * @param line the 0-based line for the breakpoint + */ + public async lineHasBreakpoint(projects: Project[], pkgPath: string, line: number) { + const workByProject = (await Promise.all( + projects.map(project => this.getBreakpointWork(project)) + )); + for (const projectWork of workByProject) { + for (let key in projectWork) { + const work = projectWork[key]; + for (const item of work) { + if (item.pkgPath === pkgPath && item.line - 1 === line) { + return true; + } + } + } + } + } + + /** + * 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. + * + * All projects should be passed in every time. + */ + public async getDiff(projects: Project[]): Promise { + this.logger.debug('getDiff'); + + //if the diff is currently running, return an empty "nothing has changed" diff + if (this.isGetDiffRunning) { + this.logger.debug('another diff is already running, exiting early'); + return { + added: [], + removed: [], + unchanged: [...this.lastState.values()] + }; + } + try { + this.isGetDiffRunning = true; + + const currentState = new Map(); + await Promise.all( + projects.map(async (project) => { + //get breakpoint data for every project + const work = await this.getBreakpointWork(project); + + this.logger.debug('[bpmanager] getDiff breakpointWork', work); + + for (const filePath in work) { + const fileWork = work[filePath]; + for (const bp of fileWork) { + bp.stagingFilePath = fileUtils.postfixFilePath(bp.stagingFilePath, project.postfix, ['.brs']); + bp.pkgPath = fileUtils.postfixFilePath(bp.pkgPath, project.postfix, ['.brs']); + const key = [ + bp.stagingFilePath, + bp.line, + bp.column, + bp.condition, + bp.hitCondition, + bp.logMessage + ].join('--'); + //clone the breakpoint and then add it to the current state + currentState.set(key, { ...bp, deviceId: this.deviceIdByDestHash.get(bp.destHash)?.deviceId }); + } + } + }) + ); + + const added = new Map(); + const removed = new Map(); + const unchanged = new Map(); + + this.logger.debug('lastState:', this.lastState); + this.logger.debug('currentState:', currentState); + + for (const key of [...currentState.keys(), ...this.lastState.keys()]) { + const inCurrent = currentState.has(key); + const inLast = this.lastState.has(key); + //no change + if (inLast && inCurrent) { + unchanged.set(key, currentState.get(key)); + + //added since last time + } else if (!inLast && inCurrent) { + added.set(key, currentState.get(key)); + + //removed since last time + } else { + removed.set(key, this.lastState.get(key)); + } + } + this.lastState = currentState; + + const result = { + added: [...added.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 + */ + private isGetDiffRunning = false; + private lastState = new Map(); + + public clearBreakpointLastState() { + this.lastState.clear(); + } +} + +export interface Diff { + added: BreakpointWorkItem[]; + removed: BreakpointWorkItem[]; + unchanged: BreakpointWorkItem[]; } -interface AugmentedSourceBreakpoint extends DebugProtocol.SourceBreakpoint { +export interface AugmentedSourceBreakpoint extends DebugProtocol.SourceBreakpoint { /** - * An ID for this breakpoint, which is used to set/unset breakpoints in the client + * The path to the source file where this breakpoint was originally set */ - id: number; + srcPath: string; /** - * Was this breakpoint added before launch? That means this breakpoint was written into the source code as a `stop` statement, - * so if users toggle this breakpoint line on and off, it should get verified every time. + * 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 */ - wasAddedBeforeLaunch: boolean; + srcHash: string; /** - * This breakpoint has been verified (i.e. we were able to set it at the given location) + * A unique ID the debug adapter generates to help send updates to the client about this breakpoint */ - verified: boolean; + id: number; /** - * Since breakpoints are written into the source code, we can't delete the `wasAddedBeforeLaunch` breakpoints, - * otherwise the non-sourcemap debugging process's line offsets could get messed up. So, for the `wasAddedBeforeLaunch` - * breakpoints, we need to mark them as hidden when the user unsets them. + * This breakpoint has been verified (i.e. we were able to set it at the given location) */ - isHidden: boolean; + verified: boolean; } -interface BreakpointWorkItem { - sourceFilePath: string; +export interface BreakpointWorkItem { + /** + * The path to the source file where this breakpoint was originally set + */ + srcPath: string; + /** + * The absolute path to the file in the staging folder + */ stagingFilePath: string; + /** + * The device path (i.e. `pkg:/source/main.brs`) + */ + pkgPath: string; + /** + * The path to the rootDir for this breakpoint + */ rootDirFilePath: string; /** * The 1-based line number */ line: number; + /** + * The device-provided breakpoint id. A missing ID means this breakpoint has not yet been verified by the device. + */ + deviceId?: number; + /** + * An id generated by the debug adapter used to identify this breakpoint in the client + */ + id: number; + /** + * 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 + */ + srcHash: string; + /** + * + */ + destHash: string; /** * The 0-based column index */ column: number; + /** + * If set, this breakpoint will only activate when this condition evaluates to true + */ condition?: string; + /** + * If set, this breakpoint will only activate once the breakpoint has been hit this many times. + */ hitCondition?: string; + /** + * If set, this breakpoint will emit a log message at runtime and will not actually stop at the breakpoint + */ logMessage?: string; + /** + * The name of the component library this belongs to. Will be null for the main project + */ + componentLibraryName?: string; /** * `sourceMap` means derived from a source map. * `fileMap` means derived from the {src;dest} entry used by roku-deploy @@ -445,3 +934,14 @@ 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/FileManager.ts b/src/managers/FileManager.ts index b46d686d..7767c9f1 100644 --- a/src/managers/FileManager.ts +++ b/src/managers/FileManager.ts @@ -16,8 +16,8 @@ export class FileManager { private cache = {} as Record; public getCodeFile(filePath: string) { - let lowerFilePath = filePath.toLowerCase(); - if (!this.cache[lowerFilePath]) { + let lowerFilePath = filePath?.toLowerCase(); + if (lowerFilePath && !this.cache[lowerFilePath]) { let fileInfo = { lines: [], functionNameMap: {} @@ -108,8 +108,7 @@ export class FileManager { let result = {}; //create a cache of all function names in this file - // eslint-disable-next-line no-cond-assign - while (match = regexp.exec(fileContents)) { + while ((match = regexp.exec(fileContents))) { let correctFunctionName = match[1]; result[correctFunctionName.toLowerCase()] = correctFunctionName; } @@ -127,7 +126,7 @@ export class FileManager { */ public getCorrectFunctionNameCase(sourceFilePath: string, functionName: string) { let fileInfo = this.getCodeFile(sourceFilePath); - return fileInfo.functionNameMap[functionName.toLowerCase()] ?? functionName; + return fileInfo?.functionNameMap[functionName?.toLowerCase()] ?? functionName; } /** diff --git a/src/managers/LocationManager.spec.ts b/src/managers/LocationManager.spec.ts index 2718f90f..ca2294ac 100644 --- a/src/managers/LocationManager.spec.ts +++ b/src/managers/LocationManager.spec.ts @@ -17,8 +17,7 @@ const sourceDirs = [ describe('LocationManager', () => { let locationManager: LocationManager; let sourceMapManager: SourceMapManager; - // eslint-disable-next-line prefer-arrow-callback - beforeEach(function beforeEach() { + beforeEach(() => { sourceMapManager = new SourceMapManager(); locationManager = new LocationManager(sourceMapManager); fsExtra.removeSync(tempDir); @@ -53,7 +52,7 @@ describe('LocationManager', () => { let location = await locationManager.getSourceLocation({ stagingFilePath: stagingFilePath, - stagingFolderPath: stagingDir, + stagingDir: stagingDir, fileMappings: [], rootDir: rootDir, lineNumber: 1, @@ -77,7 +76,7 @@ describe('LocationManager', () => { let location = await locationManager.getSourceLocation({ stagingFilePath: s`${stagingDir}/lib1.brs`, - stagingFolderPath: stagingDir, + stagingDir: stagingDir, fileMappings: [{ src: s`${rootDir}/lib1.brs`, dest: s`${stagingDir}/lib1.brs` @@ -117,7 +116,7 @@ describe('LocationManager', () => { let location = await locationManager.getSourceLocation({ stagingFilePath: stagingFilePath, - stagingFolderPath: stagingDir, + stagingDir: stagingDir, fileMappings: [], rootDir: rootDir, lineNumber: 3, @@ -143,7 +142,7 @@ describe('LocationManager', () => { let location = await locationManager.getSourceLocation({ stagingFilePath: s`${stagingDir}/lib1.brs`, - stagingFolderPath: stagingDir, + stagingDir: stagingDir, fileMappings: [{ src: s`${sourceDirs[0]}/lib1.brs`, dest: '/lib1.brs' @@ -170,7 +169,7 @@ describe('LocationManager', () => { let location = await locationManager.getSourceLocation({ stagingFilePath: s`${stagingDir}/lib1.brs`, - stagingFolderPath: stagingDir, + stagingDir: stagingDir, fileMappings: [{ src: s`${sourceDirs[1]}/lib1.brs`, dest: '/lib1.brs' @@ -196,7 +195,7 @@ describe('LocationManager', () => { let location = await locationManager.getSourceLocation({ stagingFilePath: s`${stagingDir}/lib1.brs`, - stagingFolderPath: stagingDir, + stagingDir: stagingDir, fileMappings: [{ src: s`${sourceDirs[2]}/lib1.brs`, dest: '/lib1.brs' diff --git a/src/managers/LocationManager.ts b/src/managers/LocationManager.ts index d84b8a6f..5a807ec2 100644 --- a/src/managers/LocationManager.ts +++ b/src/managers/LocationManager.ts @@ -18,7 +18,7 @@ export class LocationManager { */ public async getSourceLocation(options: GetSourceLocationOptions): Promise { let rootDir = s`${options.rootDir}`; - let stagingFolderPath = s`${options.stagingFolderPath}`; + let stagingDir = s`${options.stagingDir}`; let currentFilePath = s`${options.stagingFilePath}`; let sourceDirs = options.sourceDirs ? options.sourceDirs.map(x => s`${x}`) : []; //throw out any sourceDirs pointing the rootDir @@ -62,7 +62,7 @@ export class LocationManager { //if we have sourceDirs, rootDir is the project's OUTPUT folder, so skip looking for files there, and //instead walk backwards through sourceDirs until we find the file we want if (sourceDirs.length > 0) { - let relativeFilePath = fileUtils.getRelativePath(stagingFolderPath, currentFilePath); + let relativeFilePath = fileUtils.getRelativePath(stagingDir, currentFilePath); let sourceDirsFilePath = await fileUtils.findFirstRelativeFile(relativeFilePath, sourceDirs); //if we found a file in one of the sourceDirs, use that if (sourceDirsFilePath) { @@ -104,18 +104,18 @@ export class LocationManager { sourceLineNumber: number, sourceColumnIndex: number, sourceDirs: string[], - stagingFolderPath: string, + stagingDir: string, fileMappings: Array<{ src: string; dest: string }> ): Promise<{ type: 'fileMap' | 'sourceDirs' | 'sourceMap'; locations: SourceLocation[] }> { sourceFilePath = s`${sourceFilePath}`; sourceDirs = sourceDirs.map(x => s`${x}`); - stagingFolderPath = s`${stagingFolderPath}`; + stagingDir = s`${stagingDir}`; //look through the sourcemaps in the staging folder for any instances of this source location let locations = await this.sourceMapManager.getGeneratedLocations( await fastGlob('**/*.map', { - cwd: stagingFolderPath, + cwd: stagingDir, absolute: true }), { @@ -141,7 +141,7 @@ export class LocationManager { let parentFolderPath = fileUtils.findFirstParent(sourceFilePath, sourceDirs); if (parentFolderPath) { let relativeFilePath = fileUtils.replaceCaseInsensitive(sourceFilePath, parentFolderPath, ''); - let stagingFilePathAbsolute = path.join(stagingFolderPath, relativeFilePath); + let stagingFilePathAbsolute = path.join(stagingDir, relativeFilePath); return { type: 'sourceDirs', locations: [{ @@ -154,7 +154,7 @@ export class LocationManager { //look through the files array to see if there are any mappings that reference this file. //both `src` and `dest` are assumed to already be standardized - for (let fileMapping of fileMappings) { + for (let fileMapping of fileMappings ?? []) { if (fileMapping.src === sourceFilePath) { return { type: 'fileMap', @@ -180,7 +180,7 @@ export interface GetSourceLocationOptions { /** * The absolute path to the staging folder */ - stagingFolderPath: string; + stagingDir: string; /** * The absolute path to the file in the staging folder diff --git a/src/managers/LogManager.ts b/src/managers/LogManager.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/managers/ProjectManager.spec.ts b/src/managers/ProjectManager.spec.ts index 3d998253..6269897d 100644 --- a/src/managers/ProjectManager.spec.ts +++ b/src/managers/ProjectManager.spec.ts @@ -1,7 +1,8 @@ import { expect } from 'chai'; import * as fsExtra from 'fs-extra'; import * as path from 'path'; -import * as rokuDeploy from 'roku-deploy'; +import { util } from '../util'; +import { rokuDeploy } from 'roku-deploy'; import * as sinonActual from 'sinon'; import { fileUtils, standardizePath as s } from '../FileUtils'; import type { ComponentLibraryConstructorParams } from './ProjectManager'; @@ -9,6 +10,7 @@ import { Project, ComponentLibraryProject, ProjectManager } from './ProjectManag import { BreakpointManager } from './BreakpointManager'; import { SourceMapManager } from './SourceMapManager'; import { LocationManager } from './LocationManager'; +import * as decompress from 'decompress'; let sinon = sinonActual.createSandbox(); let n = fileUtils.standardizePath.bind(fileUtils); @@ -17,9 +19,9 @@ let cwd = fileUtils.standardizePath(process.cwd()); let tempPath = s`${cwd}/.tmp`; let rootDir = s`${tempPath}/rootDir`; let outDir = s`${tempPath}/outDir`; -let stagingFolderPath = s`${outDir}/stagingDir`; +let stagingDir = s`${outDir}/stagingDir`; let compLibOutDir = s`${outDir}/component-libraries`; -let compLibStagingFolderPath = s`${rootDir}/component-libraries/CompLibA`; +let compLibstagingDir = s`${rootDir}/component-libraries/CompLibA`; beforeEach(() => { fsExtra.ensureDirSync(tempPath); @@ -42,23 +44,27 @@ describe('ProjectManager', () => { manager = new ProjectManager(breakpointManager, locationManager); manager.mainProject = { - stagingFolderPath: stagingFolderPath + stagingDir: stagingDir }; manager.componentLibraryProjects.push({ - stagingFolderPath: compLibStagingFolderPath, + stagingDir: compLibstagingDir, libraryIndex: 1, outDir: compLibOutDir }); }); + afterEach(() => { + sinon.restore(); + }); + describe('getLineNumberOffsetByBreakpoints', () => { let filePath = 'does not matter'; it('accounts for the entry breakpoint', () => { - sinon.stub(manager.breakpointManager, 'getBreakpointsForFile').returns([{ + manager.breakpointManager['permanentBreakpointsBySrcPath'].set(filePath, [{ line: 3 }, { line: 3 - }]); + }] as any); //no offset because line is before any breakpoints expect(manager.getLineNumberOffsetByBreakpoints(filePath, 1)).to.equal(1); //after the breakpoints, should be offset by -1 @@ -66,7 +72,6 @@ describe('ProjectManager', () => { }); it('works with zero breakpoints', () => { - sinon.stub(manager.breakpointManager, 'getBreakpointsForFile').returns([]); //no offset because line is before any breakpoints expect(manager.getLineNumberOffsetByBreakpoints(filePath, 1)).to.equal(1); //after the breakpoints, should be offset by -1 @@ -116,7 +121,7 @@ describe('ProjectManager', () => { line = 12 end function */ - sinon.stub(manager.breakpointManager, 'getBreakpointsForFile').returns([ + manager.breakpointManager['permanentBreakpointsBySrcPath'].set(filePath, [ { line: 3 }, { line: 4 }, { line: 5 }, @@ -124,7 +129,7 @@ describe('ProjectManager', () => { { line: 8 }, { line: 10 }, { line: 12 } - ]); + ] as any); //no offset because line is before any breakpoints //no breakpoint expect(manager.getLineNumberOffsetByBreakpoints(filePath, 1)).to.equal(1); @@ -168,7 +173,7 @@ describe('ProjectManager', () => { expect( await manager.getStagingFileInfo('pkg:/source/main.brs') ).to.include({ - absolutePath: s`${stagingFolderPath}/source/main.brs`, + absolutePath: s`${stagingDir}/source/main.brs`, //the relative path should not include a leading slash relativePath: s`source/main.brs` }); @@ -177,13 +182,13 @@ describe('ProjectManager', () => { it(`searches for partial files in main project when '...' is encountered`, async () => { let stub = sinon.stub(fileUtils, 'findPartialFileInDirectory').callsFake((partialFilePath, directoryPath) => { expect(partialFilePath).to.equal('...ource/main.brs'); - expect(directoryPath).to.equal(manager.mainProject.stagingFolderPath); + expect(directoryPath).to.equal(manager.mainProject.stagingDir); return Promise.resolve(`source/main.brs`); }); expect( (await manager.getStagingFileInfo('...ource/main.brs')).absolutePath ).to.equal( - s`${stagingFolderPath}/source/main.brs` + s`${stagingDir}/source/main.brs` ); expect(stub.called).to.be.true; }); @@ -192,20 +197,20 @@ describe('ProjectManager', () => { expect( (await manager.getStagingFileInfo('pkg:/source/main__lib1.brs')).absolutePath ).to.equal( - s`${compLibStagingFolderPath}/source/main__lib1.brs` + s`${compLibstagingDir}/source/main__lib1.brs` ); }); it(`detects partial paths to component library filenames`, async () => { let stub = sinon.stub(fileUtils, 'findPartialFileInDirectory').callsFake((partialFilePath, directoryPath) => { expect(partialFilePath).to.equal('...ource/main__lib1.brs'); - expect(directoryPath).to.equal(manager.componentLibraryProjects[0].stagingFolderPath); + expect(directoryPath).to.equal(manager.componentLibraryProjects[0].stagingDir); return Promise.resolve(`source/main__lib1.brs`); }); let info = await manager.getStagingFileInfo('...ource/main__lib1.brs'); expect(info).to.deep.include({ relativePath: s`source/main__lib1.brs`, - absolutePath: s`${compLibStagingFolderPath}/source/main__lib1.brs` + absolutePath: s`${compLibstagingDir}/source/main__lib1.brs` }); expect(info.project).to.include({ outDir: compLibOutDir @@ -216,6 +221,12 @@ describe('ProjectManager', () => { }); describe('getSourceLocation', () => { + it(`does not crash when file is missing`, async () => { + manager.mainProject.fileMappings = []; + let sourceLocation = await manager.getSourceLocation('pkg:/source/file-we-dont-know-about.brs', 1); + expect(n(sourceLocation.filePath)).to.equal(n(`${stagingDir}/source/file-we-dont-know-about.brs`)); + }); + it('handles truncated paths', async () => { //mock fsExtra so we don't have to create actual files sinon.stub(fsExtra as any, 'pathExists').callsFake((filePath: string) => { @@ -230,13 +241,13 @@ describe('ProjectManager', () => { 'source/file2.brs' ])); manager.mainProject.rootDir = rootDir; - manager.mainProject.stagingFolderPath = stagingFolderPath; + manager.mainProject.stagingDir = stagingDir; manager.mainProject.fileMappings = [{ src: s`${rootDir}/source/file1.brs`, - dest: s`${stagingFolderPath}/source/file1.brs` + dest: s`${stagingDir}/source/file1.brs` }, { src: s`${rootDir}/source/file2.brs`, - dest: s`${stagingFolderPath}/source/file2.brs` + dest: s`${stagingDir}/source/file2.brs` }]; let sourceLocation = await manager.getSourceLocation('...rce/file1.brs', 1); @@ -257,13 +268,13 @@ describe('ProjectManager', () => { } }); manager.mainProject.rootDir = rootDir; - manager.mainProject.stagingFolderPath = stagingFolderPath; + manager.mainProject.stagingDir = stagingDir; manager.mainProject.fileMappings = [{ src: s`${rootDir}/source/file1.brs`, - dest: s`${stagingFolderPath}/source/file1.brs` + dest: s`${stagingDir}/source/file1.brs` }, { src: s`${rootDir}/source/file2.brs`, - dest: s`${stagingFolderPath}/source/file2.brs` + dest: s`${stagingDir}/source/file2.brs` }]; let sourceLocation = await manager.getSourceLocation('pkg:source/file1.brs', 1); @@ -275,7 +286,6 @@ describe('ProjectManager', () => { sourceLocation = await manager.getSourceLocation('pkg:/source/file2.brs', 1); expect(n(sourceLocation.filePath)).to.equal(n(`${rootDir}/source/file2.brs`)); }); - }); }); @@ -294,11 +304,15 @@ describe('Project', () => { injectRdbOnDeviceComponent: true, rdbFilesBasePath: rdbFilesBasePath, sourceDirs: [s`${cwd}/source1`], - stagingFolderPath: stagingFolderPath, + stagingDir: stagingDir, raleTrackerTaskFileLocation: 'z' }); }); + afterEach(() => { + sinon.restore(); + }); + it('copies the necessary properties onto the instance', () => { expect(project.rootDir).to.equal(cwd); expect(project.files).to.eql(['a']); @@ -306,7 +320,7 @@ describe('Project', () => { expect(project.injectRaleTrackerTask).to.equal(true); expect(project.outDir).to.eql(outDir); expect(project.sourceDirs).to.eql([s`${cwd}/source1`]); - expect(project.stagingFolderPath).to.eql(stagingFolderPath); + expect(project.stagingDir).to.eql(stagingDir); expect(project.raleTrackerTaskFileLocation).to.eql('z'); expect(project.injectRdbOnDeviceComponent).to.equal(true); expect(project.rdbFilesBasePath).to.eql(rdbFilesBasePath); @@ -322,17 +336,17 @@ describe('Project', () => { project.raleTrackerTaskFileLocation = undefined; project.rootDir = rootDir; project.outDir = outDir; - project.stagingFolderPath = stagingFolderPath; + project.stagingDir = stagingDir; fsExtra.ensureDirSync(project.rootDir); fsExtra.ensureDirSync(project.outDir); - fsExtra.ensureDirSync(project.stagingFolderPath); + fsExtra.ensureDirSync(project.stagingDir); fsExtra.writeFileSync(s`${project.rootDir}/manifest`, 'bs_const=b=true'); project.files = [ 'manifest' ]; await project.stage(); - expect(fsExtra.pathExistsSync(`${stagingFolderPath}/manifest`)).to.be.true; + expect(fsExtra.pathExistsSync(`${stagingDir}/manifest`)).to.be.true; }); }); @@ -429,7 +443,7 @@ describe('Project', () => { let filePath = s`${folder}/main.${fileExt}`; fsExtra.writeFileSync(filePath, fileContents); - project.stagingFolderPath = folder; + project.stagingDir = folder; project.injectRaleTrackerTask = true; //these file contents don't actually matter project.raleTrackerTaskFileLocation = raleTrackerTaskFileLocation; @@ -442,7 +456,7 @@ describe('Project', () => { fsExtra.ensureDirSync(tempPath); fsExtra.writeFileSync(`${tempPath}/RALE.xml`, 'test contents'); await doTest(`sub main()\nend sub`, `sub main()\nend sub`); - expect(fsExtra.pathExistsSync(s`${project.stagingFolderPath}/components/TrackerTask.xml`), 'TrackerTask.xml was not copied to staging').to.be.true; + expect(fsExtra.pathExistsSync(s`${project.stagingDir}/components/TrackerTask.xml`), 'TrackerTask.xml was not copied to staging').to.be.true; }); it('works for inline comments brs files', async () => { @@ -542,7 +556,7 @@ describe('Project', () => { let filePath = s`${folder}/main.${fileExt}`; fsExtra.writeFileSync(filePath, fileContents); - project.stagingFolderPath = folder; + project.stagingDir = folder; project.injectRdbOnDeviceComponent = injectRdbOnDeviceComponent; project.rdbFilesBasePath = rdbFilesBasePath; await project.copyAndTransformRDB(); @@ -553,8 +567,8 @@ describe('Project', () => { it('copies the RDB files', async () => { fsExtra.ensureDirSync(tempPath); await doTest(`sub main()\nend sub`, `sub main()\nend sub`); - expect(fsExtra.pathExistsSync(s`${project.stagingFolderPath}/${sourceFileRelativePath}`), `${sourceFileRelativePath} was not copied to staging`).to.be.true; - expect(fsExtra.pathExistsSync(s`${project.stagingFolderPath}/${componentsFileRelativePath}`), `${componentsFileRelativePath} was not copied to staging`).to.be.true; + expect(fsExtra.pathExistsSync(s`${project.stagingDir}/${sourceFileRelativePath}`), `${sourceFileRelativePath} was not copied to staging`).to.be.true; + expect(fsExtra.pathExistsSync(s`${project.stagingDir}/${componentsFileRelativePath}`), `${componentsFileRelativePath} was not copied to staging`).to.be.true; }); it('works for inline comments brs files', async () => { @@ -573,8 +587,8 @@ describe('Project', () => { // let brsSample = `\nsub main()\n screen.show\n ' ${Project.RDB_ODC_ENTRY}\nend sub`; // project.injectRdbOnDeviceComponent = false // await doTest(brsSample, brsSample, 'brs', false); - // expect(fsExtra.pathExistsSync(s`${project.stagingFolderPath}/${sourceFileRelativePath}`), `${sourceFileRelativePath} should not have been copied to staging`).to.be.false; - // expect(fsExtra.pathExistsSync(s`${project.stagingFolderPath}/${componentsFileRelativePath}`), `${componentsFileRelativePath} should not have been copied to staging`).to.be.false; + // expect(fsExtra.pathExistsSync(s`${project.stagingDir}/${sourceFileRelativePath}`), `${sourceFileRelativePath} should not have been copied to staging`).to.be.false; + // expect(fsExtra.pathExistsSync(s`${project.stagingDir}/${componentsFileRelativePath}`), `${componentsFileRelativePath} should not have been copied to staging`).to.be.false; // }); it('works for in line comments in xml files', async () => { @@ -634,6 +648,37 @@ describe('Project', () => { await doTest(xmlSample.replace('', `sub init()\n m.something = true\n ' ${Project.RDB_ODC_ENTRY} \n end sub`), expectedXml, 'xml'); }); }); + + + describe('zipPackage', () => { + it('excludes sourcemaps', async () => { + fsExtra.outputFileSync(`${project.stagingDir}/manifest`, '#stuff'); + fsExtra.outputFileSync(`${project.stagingDir}/source/main.brs`, 'sub main() : end sub'); + fsExtra.outputFileSync(`${project.stagingDir}/source/main.brs.map`, '{}'); + await project.zipPackage({ retainStagingFolder: true }); + const zipPath = path.join( + project.outDir, + fsExtra.readdirSync(project.outDir).find(x => x?.toLowerCase().endsWith('.zip')) + ); + + await decompress(zipPath, `${tempPath}/extracted`); + expect(fsExtra.pathExistsSync(`${tempPath}/extracted/manifest`)).to.be.true; + expect(fsExtra.pathExistsSync(`${tempPath}/extracted/source/main.brs`)).to.be.true; + expect(fsExtra.pathExistsSync(`${tempPath}/extracted/source/main.brs.map`)).to.be.false; + }); + + it('uses "packagePath" when specified', async () => { + fsExtra.outputFileSync(`${project.stagingDir}/manifest`, '#stuff'); + fsExtra.outputFileSync(`${project.stagingDir}/source/main.brs`, 'sub main() : end sub'); + project.packagePath = s`${tempPath}/package/path.zip`; + await project.zipPackage({ retainStagingFolder: true }); + + await decompress(project.packagePath, `${tempPath}/extracted`); + expect(fsExtra.pathExistsSync(`${tempPath}/extracted/manifest`)).to.be.true; + expect(fsExtra.pathExistsSync(`${tempPath}/extracted/source/main.brs`)).to.be.true; + }); + }); + }); describe('ComponentLibraryProject', () => { @@ -646,7 +691,7 @@ describe('ComponentLibraryProject', () => { bsConst: { b: true }, injectRaleTrackerTask: true, sourceDirs: [s`${tempPath}/source1`], - stagingFolderPath: s`${outDir}/complib1-staging`, + stagingDir: s`${outDir}/complib1-staging`, raleTrackerTaskFileLocation: 'z', libraryIndex: 0, outFile: 'PrettyComponent.zip' @@ -662,9 +707,57 @@ describe('ComponentLibraryProject', () => { }); }); + describe('addPostFixToPath', () => { + it('adds postfix if path is 1) pkg:/ or 2) relative - no spaces in url', async () => { + let project = new ComponentLibraryProject(params); + project.fileMappings = []; + fsExtra.outputFileSync(`${params.stagingDir}/source/main.brs`, ''); + fsExtra.outputFileSync(`${params.stagingDir}/components/Component1.xml`, ` + +