diff --git a/.coderabbit.yml b/.coderabbit.yml new file mode 100644 index 00000000..594b06b1 --- /dev/null +++ b/.coderabbit.yml @@ -0,0 +1,28 @@ +language: "en" +early_access: false +reviews: + request_changes_workflow: false + high_level_summary: true + poem: true + review_status: true + collapse_walkthrough: false + path_filters: + - "!**/.xml" + path_instructions: + - path: "**/*.js" + instructions: "Review the JavaScript code for conformity with the Google JavaScript style guide, highlighting any deviations." + - path: "**/*.ts" + instructions: "Review the Typescript code for conformity with industry standards and best practices, highlighting any deviations." + - path: "**/*.py" + instructions: | + "Review the Python code for conformity with Python best practices and industry standards, highlighting any deviations." + auto_review: + enabled: true + ignore_title_keywords: + - "WIP" + - "DO NOT MERGE" + drafts: false + base_branches: + - "develop" +chat: + auto_reply: true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..ac291699 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,19 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], + + "rules": { + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/consistent-type-definitions": ["error", "type"] + }, + + "env": { + "browser": true, + "es2021": true + } +} \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..bb8a0015 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,24 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "rules": { + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "import", + "format": ["camelCase", "PascalCase"] + } + ], + "@typescript-eslint/semi": "warn", + "curly": "warn", + "eqeqeq": "warn", + "no-throw-literal": "warn", + "semi": "off" + }, + "ignorePatterns": ["out", "dist", "**/*.d.ts"] +} diff --git a/.github/actions/lint/action.yml b/.github/actions/lint/action.yml new file mode 100644 index 00000000..bb969c80 --- /dev/null +++ b/.github/actions/lint/action.yml @@ -0,0 +1,40 @@ +--- +name: Lint +description: Lint TypeScript and Python code +runs: + using: composite + steps: + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: npm + cache-dependency-path: package-lock.json + - name: Install Node dependencies + run: npm ci + shell: bash + - name: Lint TypeScript code + run: npm run lint + shell: bash + - name: Check TypeScript format + run: npm run format-check + shell: bash + - name: Install Python + uses: actions/setup-python@v4 + with: + python-version: '3.8' + - name: Pip cache + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-lint-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip-lint- + - name: Install Python dependencies + run: | + python -m pip install -U pip + pip install -r requirements-dev.txt + shell: bash + - name: Lint Python and YAML code + run: ./scripts/lint.sh + shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..3e845897 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +--- +name: VSCode Extension CI +on: [push, pull_request] +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Lint + uses: ./.github/actions/lint + build-and-test: + needs: lint + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Install Node.js + uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: npm + cache-dependency-path: package-lock.json + - name: Install Node dependencies + run: npm ci + - name: Compile TS tests + run: npm run pretest + - name: Run headless test + uses: coactions/setup-xvfb@v1 + with: + run: npm test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ca52a056 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +--- +name: Publish VSCode Extension +on: + release: + types: [created] +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + - name: Install Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + cache: npm + - name: Install dependencies + run: npm ci + - name: Build Extension + run: npm run package + - name: Package Extension + run: npm run vsce-package + - name: Publish Extension + if: success() && startsWith(github.ref, 'refs/tags/') + run: npm run deploy + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + - name: Generate Changelog + if: success() && startsWith(github.ref, 'refs/tags/') + run: | + git log $(git describe --tags --abbrev=0)..HEAD --oneline > CHANGELOG.txt + cat CHANGELOG.txt + - name: Create GitHub Release + if: success() && startsWith(github.ref, 'refs/tags/') + uses: ncipollo/release-action@v1 + with: + artifacts: zenml.vsix + bodyFile: CHANGELOG.txt + tag: ${{ github.ref_name }} + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..51484850 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +.DS_Store +Thumbs.db +.vscode-test/ +.idea/ +.eslintcache +lib-cov +*.log +*.log* +pids + +# Node +.npm/ +node_modules/ +npm-debug.log + +# Build outputs +dist/ +out/ +build/ +*.tsbuildinfo +.history/ +dag-packed.js +dag-packed.js.map + +# env +.env +.env* + +# Output of 'npm pack' +*.tgz + +# mypy +.mypy_cache/ + +# From vscode-python-tools-extension-template +*.vsix +.venv/ +.vs/ +.nox/ +bundled/libs/ +**/__pycache__ +**/.pytest_cache +**/.vs \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..fbe6ca4f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "proseWrap": "preserve", + "singleQuote": true, + "arrowParens": "avoid", + "trailingComma": "es5", + "bracketSpacing": true +} diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..8749d63a --- /dev/null +++ b/.pylintrc @@ -0,0 +1,7 @@ +[MESSAGES CONTROL] +disable= + C0103, # Name doesn't conform to naming style + C0415, # Import outside toplevel + W0613, # Unused argument + R0801, # Similar lines in multiple files + R0903, # Too few public methods \ No newline at end of file diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000..7bd5f0bf --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 20.10.0 diff --git a/.vscode-test.mjs b/.vscode-test.mjs new file mode 100644 index 00000000..b62ba25f --- /dev/null +++ b/.vscode-test.mjs @@ -0,0 +1,5 @@ +import { defineConfig } from '@vscode/test-cli'; + +export default defineConfig({ + files: 'out/test/**/*.test.js', +}); diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..9e5e0b73 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "dbaeumer.vscode-eslint", + "amodio.tsl-problem-matcher", + "ms-vscode.extension-test-runner" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..fe54ee0f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// 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": "${defaultBuildTask}", + "timeout": 20000 + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..0e65fd08 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "python.defaultInterpreterPath": "", + "files.exclude": { + "out": false, // set this to true to hide the "out" folder with the compiled JS files + "dist": false // set this to true to hide the "dist" folder with the compiled JS files + }, + "search.exclude": { + "out": true, // set this to false to include "out" folder in search results + "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", + "python.testing.pytestArgs": ["src/test/python_tests"], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python.testing.cwd": "${workspaceFolder}", + "python.analysis.extraPaths": ["bundled/libs", "bundled/tool"] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..59cb4ae2 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,45 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$ts-webpack-watch", + "isBackground": true, + "presentation": { + "reveal": "never", + "group": "watchers" + }, + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "type": "npm", + "script": "watch-tests", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never", + "group": "watchers" + }, + "group": "build" + }, + { + "label": "tasks: watch-tests", + "dependsOn": ["npm: watch", "npm: watch-tests"], + "problemMatcher": [] + }, + { + "type": "npm", + "script": "compile", + "group": "build", + "problemMatcher": [], + "label": "npm: compile", + "detail": "webpack" + } + ] +} diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 00000000..fa9763b0 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,15 @@ +.vscode/** +.vscode-test/** +out/** +node_modules/** +src/** +.gitignore +.yarnrc +webpack.config.js +vsc-extension-quickstart.md +**/tsconfig.json +**/.eslintrc.json +**/*.map +**/*.js.map +**/*.ts +**/.vscode-test.* \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..60358ad0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,100 @@ +# Contributing to ZenML VSCode Extension + +We appreciate your interest in contributing to the ZenML VSCode extension! This guide will help you get started with setting up your development environment, making changes, and proposing those changes back to the project. By following these guidelines, you'll ensure a smooth and efficient contribution process. + +## Setting Up Your Development Environment + +1. **Fork and Clone**: Fork the [zenml-io/vscode-zenml repository](https://github.com/zenml-io/vscode-zenml) and clone it to your local machine. + +```bash +git clone https://github.com/YOUR_USERNAME/vscode-zenml.git +git checkout develop +``` + +2. **Install Dependencies**: Navigate to the cloned repository directory and install the required dependencies. + +```bash +cd vscode-zenml +npm install +``` + +3. **Compile the Project**: Build the TypeScript source code into JavaScript. + +```bash +npm run compile +``` + +### Python Environment Setup + +The extension's Python functionality requires setting up a corresponding Python environment. + +1. Create and activate a Python virtual environment using Python 3.8 or greater. (e.g., `python -m venv .venv` on Windows, or `python3 -m venv .venv` on Unix-based systems). +2. Install `nox` for environment management. + +```bash +python -m pip install nox +``` + +3. Use `nox` to set up the environment. + +```bash +nox --session setup +``` + +4. Install any Python dependencies specified in `requirements.txt`. + +```bash +pip install -r requirements.txt +``` + +## Development Workflow + +- **Running the Extension**: Press `F5` to open a new VSCode window with the + extension running, or click the `Start Debugging` button in the VSCode menubar + under the `Run` menu. +- **Making Changes**: Edit the source code. The TypeScript code is in the `src` directory, and Python logic for the LSP server is in `bundled/tool`. + +### Testing + +- **Writing Tests**: Add tests in the `src/test` directory. Follow the naming convention `*.test.ts` for test files. +- **Running Tests**: Use the provided npm scripts to compile and run tests. + +```bash +npm run test +``` + +### Debugging + +- **VSCode Debug Console**: Utilize the debug console in the VSCode development window for troubleshooting and inspecting values. +- **Extension Host Logs**: Review the extension host logs for runtime errors or unexpected behavior. + +## Contributing Changes + +1. **Create a Branch**: Make your changes in a new git branch based on the `develop` branch. + +```bash +git checkout -b feature/your-feature-name +``` + +2. **Commit Your Changes**: Write clear, concise commit messages following the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) guidelines. +3. **Push to Your Fork**: Push your branch to your fork on GitHub. + +```bash +git push origin feature/your-feature-name +``` + +4. **Open a Pull Request**: Go to the original `zenml-io/vscode-zenml` repository and create a pull request from your feature branch. Please follow our [contribution guidelines](https://github.com/zenml-io/zenml/blob/develop/CONTRIBUTING.md) for more details on proposing pull requests. + +## Troubleshooting Common Issues + +- Ensure all dependencies are up to date and compatible. +- Rebuild the project (`npm run compile`) after making changes. +- Reset your development environment if encountering persistent issues by re-running `nox` setup commands and reinstalling dependencies. +- You can also run the `scripts/clear_and_compile.sh` script, which will delete the cache, `dist` folder, and recompile automatically. +- Check the [ZenML documentation](https://docs.zenml.io) and [GitHub issues](https://github.com/zenml-io/zenml/issues) for common problems and solutions. + +### Additional Resources + +- [ZenML VSCode Extension Repository](https://github.com/zenml-io/vscode-zenml) +- [ZenML Documentation](https://docs.zenml.io) +- [ZenML Slack Community](https://zenml.io/slack) diff --git a/README.md b/README.md index ae7c0768..3dedfd85 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,70 @@ -# vscode-zenml -VSCode extension for ZenML +# ZenML Extension for Visual Studio Code + +![](https://img.shields.io/github/license/zenml-io/vscode-zenml) + +![](resources/zenml-extension.gif) + +The ZenML VSCode extension seamlessly integrates with [ZenML](https://github.com/zenml-io/zenml) to enhance your MLOps workflow within VSCode. It is designed to accurately mirror the current state of your ZenML environment within your IDE, ensuring a smooth and integrated experience. + +## Features + +- **Server, Stacks, and Pipeline Runs Views**: Interact directly with ML stacks, pipeline runs, and server configurations from the Activity Bar. +- **DAG Visualization for Pipeline Runs**: Explore Directed Acyclic Graphs for each pipeline view directly directly on the Activity Bar. +- **Python Tool Integration**: Utilizes a Language Server Protocol (LSP) server for real-time synchronization with the ZenML environment. +- **Real-Time Configuration Monitoring**: Leverages `watchdog` to dynamically update configurations, keeping the extension in sync with your ZenML setup. +- **Status Bar**: Display the current stack name and connection status. You can + also change your active stack from the status bar. + +## Getting Started + +Note that you'll need to have [ZenML](https://github.com/zenml-io/zenml) installed in your Python environment to use +this extension and your Python version needs to be 3.8 or greater. + +1. **Install the Extension**: Search for "ZenML" in the VSCode Extensions view (`Ctrl+Shift+X`) and install it. +2. **Connect to ZenML Server**: Use the `ZenML: Connect` command to connect to your ZenML server. +3. **Explore ZenML Views**: Navigate to the ZenML activity bar to access the Server, Stacks, and Pipeline Runs views. + +## Using ZenML in VSCode + +- **Manage Server Connections**: Connect or disconnect from ZenML servers and refresh server status. +- **Stack Operations**: View stack details, register, update, delete, copy, or set active stacks directly from VSCode. +- **Stack Component Operations**: View stack component details, register, update, or delete stack components directly from VSCode. +- **Pipeline Runs**: Monitor and manage pipeline runs, including deleting runs from the system and rendering DAGs. +- **Environment Information**: Get detailed snapshots of the development environment, aiding troubleshooting. + +### DAG Visualization + +![DAG Visualization Example](resources/zenml-extension-dag.gif) + +- **Directed Acyclic Graph rendering** + - click on the Render Dag context action (labeled 1 in above image) next to the pipeline run you want to render. This will render the DAG in the editor window. +- **Graph manuevering** + - Panning the graph can be done by clicking and dragging anywhere on the graph. + - Zooming can be controlled by the mousewheel, the control panel (labeled 2 in the above graph) or double-clicking anywhere there is not a node. + - Mousing over a node will highlight all edges being output by that node + - Clicking a node will display the data related to it in the ZenML panel view (labeled 3 in the above image) + - Double-clicking a node will open the dashboard in a web browser to either the pipeline run or the artifact version. + +## Requirements + +- **ZenML Installation:** ZenML needs to be installed in the local Python environment associated with the Python interpreter selected in the current VS Code workspace. This extension interacts directly with your ZenML environment, so ensuring that ZenML is installed and properly configured is essential. +- **ZenML Version**: To ensure full functionality and compatibility, make sure you have ZenML version 0.63.0 or newer. +- **Python Version**: Python 3.8 or greater is required for the operation of the LSP server, which is a part of this extension. + +## Feedback and Contributions + +Your feedback and contributions are welcome! Please refer to our [contribution +guidelines](https://github.com/zenml-io/vscode-zenml/blob/develop/CONTRIBUTING.md) for more +information. + +For any further questions or issues, please reach out to us in our [Slack +Community](https://zenml.io/slack-invite). To learn more about ZenML, +please visit our [website](https://zenml.io/) and read [our documentation](https://docs.zenml.io). + +## License + +Apache-2.0 + +--- + +ZenML © 2024, ZenML. Released under the [Apache-2.0 License](LICENSE). diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..2ef9af80 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,62 @@ +# Release Process + +This document describes the process of publishing releases for our VS Code extension and provides an explanation of the GitHub Actions workflow file. + +## Overview + +The release process is automated using GitHub Actions. When a new release is created on GitHub, it triggers the release workflow defined in `.github/workflows/release.yml`. The workflow performs the following steps: + +1. Checks out the repository. +2. Installs Node.js and the required dependencies. +3. Builds the extension using webpack. +4. Packages the extension into a `.vsix` file. +5. Publishes the extension to the Visual Studio Code Marketplace. +6. Generates a changelog based on the commit messages. +7. Creates a GitHub release with the packaged extension file as an artifact and the changelog. + +## Prerequisites + +Before creating a release, ensure that: + +- The extension is properly configured and builds successfully. +- The Personal Access Token (PAT) is set as a repository secret named `VSCE_PAT`. + +## Creating a Release + +To create a new release: + +1. Go to the GitHub repository page. +2. Click on the "Releases" tab. +3. Click on the "Draft a new release" button. +4. Enter the tag version for the release (e.g., `v1.0.0`). +5. Set the release title and description. +6. Choose the appropriate release type (e.g., pre-release or stable release). +7. Click on the "Publish release" button. + +Creating the release will trigger the release workflow automatically. + +## Workflow File Explanation + +The release workflow is defined in `.github/workflows/release.yml`. Here's an explanation of each step in the workflow: + +1. **Checkout Repository**: This step checks out the repository using the `actions/checkout@v2` action. + +2. **Install Node.js**: This step sets up Node.js using the `actions/setup-node@v2` action. It specifies the Node.js version and enables caching of npm dependencies. + +3. **Install dependencies**: This step runs `npm ci` to install the project dependencies. + +4. **Build Extension**: This step runs `npm run package` to build the extension using webpack. + +5. **Package Extension**: This step runs `npm run vsce-package` to package the extension into a `.vsix` file named `zenml.vsix`. + +6. **Publish Extension**: This step runs `npm run deploy` to publish the extension to the Visual Studio Code Marketplace. It uses the `VSCE_PAT` secret for authentication. This step only runs if the previous steps succeeded and the workflow was triggered by a new tag push. + +7. **Generate Changelog**: This step generates a changelog by running `git log` to retrieve the commit messages between the latest tag and the current commit. The changelog is saved in a file named `CHANGELOG.txt`. This step only runs if the previous steps succeeded and the workflow was triggered by a new tag push. + +8. **Create GitHub Release**: This step uses the `ncipollo/release-action@v1` action to create a GitHub release. It attaches the `zenml.vsix` file as an artifact, includes the changelog, and sets the release tag based on the pushed tag. This step only runs if the previous steps succeeded and the workflow was triggered by a new tag push. + +## Conclusion + +The provided GitHub Actions workflow automates the publishing of the ZenML VSCode extension. The workflow ensures that the extension is built, packaged, published to the marketplace, and a GitHub release is created with the necessary artifacts and changelog. + +Remember to keep the extension code up to date, maintain the required dependencies, and test the extension thoroughly before creating a release. diff --git a/bundled/tool/__init__.py b/bundled/tool/__init__.py new file mode 100644 index 00000000..d83bd1ae --- /dev/null +++ b/bundled/tool/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. diff --git a/bundled/tool/_debug_server.py b/bundled/tool/_debug_server.py new file mode 100644 index 00000000..5b9cb755 --- /dev/null +++ b/bundled/tool/_debug_server.py @@ -0,0 +1,52 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +"""Debugging support for LSP.""" + +import os +import pathlib +import runpy +import sys + + +def update_sys_path(path_to_add: str) -> None: + """Add given path to `sys.path`.""" + if path_to_add not in sys.path and os.path.isdir(path_to_add): + sys.path.append(path_to_add) + + +# Ensure debugger is loaded before we load anything else, to debug initialization. +debugger_path = os.getenv("DEBUGPY_PATH", None) +if debugger_path: + if debugger_path.endswith("debugpy"): + debugger_path = os.fspath(pathlib.Path(debugger_path).parent) + + # pylint: disable=wrong-import-position,import-error + import debugpy + + update_sys_path(debugger_path) + + # pylint: disable=wrong-import-position,import-error + + # 5678 is the default port, If you need to change it update it here + # and in launch.json. + debugpy.connect(5678) + + # This will ensure that execution is paused as soon as the debugger + # connects to VS Code. If you don't want to pause here comment this + # line and set breakpoints as appropriate. + debugpy.breakpoint() + +SERVER_PATH = os.fspath(pathlib.Path(__file__).parent / "lsp_server.py") +# NOTE: Set breakpoint in `lsp_server.py` before continuing. +runpy.run_path(SERVER_PATH, run_name="__main__") diff --git a/bundled/tool/constants.py b/bundled/tool/constants.py new file mode 100644 index 00000000..a3c87a7e --- /dev/null +++ b/bundled/tool/constants.py @@ -0,0 +1,26 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +"""Constants for ZenML Tool""" + +TOOL_MODULE_NAME = "zenml-python" +TOOL_DISPLAY_NAME = "ZenML" +MIN_ZENML_VERSION = "0.55.0" + +"""Constants for ZenML Notifications and Events""" + +IS_ZENML_INSTALLED = "zenml/isInstalled" +ZENML_CLIENT_INITIALIZED = "zenml/clientInitialized" +ZENML_SERVER_CHANGED = "zenml/serverChanged" +ZENML_STACK_CHANGED = "zenml/stackChanged" +ZENML_REQUIREMENTS_NOT_MET = "zenml/requirementsNotMet" diff --git a/bundled/tool/lazy_import.py b/bundled/tool/lazy_import.py new file mode 100644 index 00000000..2cce2c4b --- /dev/null +++ b/bundled/tool/lazy_import.py @@ -0,0 +1,87 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. +""" +Utilities for lazy importing and temporary suppression of stdout and logging. +Reduces noise when integrating with logging-heavy systems like LSP. +Useful for quieter module loads and command executions. +""" + +import importlib +import logging +import os +import sys +from contextlib import contextmanager + + +@contextmanager +def suppress_logging_temporarily(level=logging.ERROR): + """ + Temporarily elevates logging level and suppresses stdout to + minimize console output during imports. + + Parameters: + level (int): Temporary logging level (default: ERROR). + + Yields: + None: While suppressing stdout. + """ + original_level = logging.root.level + original_stdout = sys.stdout + logging.root.setLevel(level) + with open(os.devnull, "w", encoding="utf-8") as fnull: + sys.stdout = fnull + try: + yield + finally: + sys.stdout = original_stdout + logging.root.setLevel(original_level) + + +@contextmanager +def suppress_stdout_temporarily(): + """ + This context manager suppresses stdout for LSP commands, + silencing unnecessary or unwanted output during execution. + + Yields: + None: While suppressing stdout. + """ + with open(os.devnull, "w", encoding="utf-8") as fnull: + original_stdout = sys.stdout + original_stderr = sys.stderr + sys.stdout = fnull + sys.stderr = fnull + try: + yield + finally: + sys.stdout = original_stdout + sys.stderr = original_stderr + + +def lazy_import(module_name, class_name=None): + """ + Lazily imports a module or class, suppressing ZenML log output + to minimize initialization time and noise. + + Args: + module_name (str): The name of the module to import. + class_name (str, optional): The class name within the module. Defaults to None. + + Returns: + The imported module or class. + """ + with suppress_logging_temporarily(): + module = importlib.import_module(module_name) + if class_name: + return getattr(module, class_name) + return module diff --git a/bundled/tool/lsp_jsonrpc.py b/bundled/tool/lsp_jsonrpc.py new file mode 100644 index 00000000..59bc3c70 --- /dev/null +++ b/bundled/tool/lsp_jsonrpc.py @@ -0,0 +1,277 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +"""Light-weight JSON-RPC over standard IO.""" + + +import atexit +import io +import json +import pathlib +import subprocess +import threading +import uuid +from concurrent.futures import ThreadPoolExecutor +from typing import BinaryIO, Dict, Optional, Sequence, Union, cast + +CONTENT_LENGTH = "Content-Length: " +RUNNER_SCRIPT = str(pathlib.Path(__file__).parent / "lsp_runner.py") + + +def to_str(text) -> str: + """Convert bytes to string as needed.""" + return text.decode("utf-8") if isinstance(text, bytes) else text + + +class StreamClosedException(Exception): + """JSON RPC stream is closed.""" + + pass # pylint: disable=unnecessary-pass + + +class JsonWriter: + """Manages writing JSON-RPC messages to the writer stream.""" + + def __init__(self, writer: io.TextIOWrapper): + self._writer = writer + self._lock = threading.Lock() + + def close(self): + """Closes the underlying writer stream.""" + with self._lock: + if not self._writer.closed: + self._writer.close() + + def write(self, data): + """Writes given data to stream in JSON-RPC format.""" + if self._writer.closed: + raise StreamClosedException() + + with self._lock: + content = json.dumps(data) + length = len(content.encode("utf-8")) + self._writer.write(f"{CONTENT_LENGTH}{length}\r\n\r\n{content}") + # self._writer.write( + # f"{CONTENT_LENGTH}{length}\r\n\r\n{content}".encode("utf-8") + # ) + self._writer.flush() + + +class JsonReader: + """Manages reading JSON-RPC messages from stream.""" + + def __init__(self, reader: io.TextIOWrapper): + self._reader = reader + + def close(self): + """Closes the underlying reader stream.""" + if not self._reader.closed: + self._reader.close() + + def read(self): + """Reads data from the stream in JSON-RPC format.""" + if self._reader.closed: + raise StreamClosedException + length = None + while not length: + line = to_str(self._readline()) + if line.startswith(CONTENT_LENGTH): + length = int(line[len(CONTENT_LENGTH) :]) + + line = to_str(self._readline()).strip() + while line: + line = to_str(self._readline()).strip() + + content = to_str(self._reader.read(length)) + return json.loads(content) + + def _readline(self): + line = self._reader.readline() + if not line: + raise EOFError + return line + + +class JsonRpc: + """Manages sending and receiving data over JSON-RPC.""" + + def __init__(self, reader: io.TextIOWrapper, writer: io.TextIOWrapper): + self._reader = JsonReader(reader) + self._writer = JsonWriter(writer) + + def close(self): + """Closes the underlying streams.""" + try: + self._reader.close() + except: # noqa: E722 # pylint: disable=bare-except + pass + try: + self._writer.close() + except: # noqa: E722 # pylint: disable=bare-except + pass + + def send_data(self, data): + """Send given data in JSON-RPC format.""" + self._writer.write(data) + + def receive_data(self): + """Receive data in JSON-RPC format.""" + return self._reader.read() + + +def create_json_rpc(readable: BinaryIO, writable: BinaryIO) -> JsonRpc: + """Creates JSON-RPC wrapper for the readable and writable streams.""" + text_readable = io.TextIOWrapper(readable, encoding="utf-8") + text_writable = io.TextIOWrapper(writable, encoding="utf-8") + return JsonRpc(text_readable, text_writable) + + +class ProcessManager: + """Manages sub-processes launched for running tools.""" + + def __init__(self): + self._args: Dict[str, Sequence[str]] = {} + self._processes: Dict[str, subprocess.Popen] = {} + self._rpc: Dict[str, JsonRpc] = {} + self._lock = threading.Lock() + self._thread_pool = ThreadPoolExecutor(10) + + def stop_all_processes(self): + """Send exit command to all processes and shutdown transport.""" + for i in self._rpc.values(): + try: + i.send_data({"id": str(uuid.uuid4()), "method": "exit"}) + except: # noqa: E722 # pylint: disable=bare-except + pass + self._thread_pool.shutdown(wait=False) + + def start_process(self, workspace: str, args: Sequence[str], cwd: str) -> None: + """Starts a process and establishes JSON-RPC communication over stdio.""" + # pylint: disable=consider-using-with + proc = subprocess.Popen( + args, + cwd=cwd, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + ) + + # Use cast to assure mypy that stdout and stdin are not None + stdout = cast(BinaryIO, proc.stdout) + stdin = cast(BinaryIO, proc.stdin) + + self._processes[workspace] = proc + self._rpc[workspace] = create_json_rpc(stdout, stdin) + + def _monitor_process(): + proc.wait() + with self._lock: + try: + del self._processes[workspace] + rpc = self._rpc.pop(workspace) + rpc.close() + except: # noqa: E722 # pylint: disable=bare-except + pass + + self._thread_pool.submit(_monitor_process) + + def get_json_rpc(self, workspace: str) -> JsonRpc: + """Gets the JSON-RPC wrapper for the a given id.""" + with self._lock: + if workspace in self._rpc: + return self._rpc[workspace] + raise StreamClosedException() + + +_process_manager = ProcessManager() +atexit.register(_process_manager.stop_all_processes) + + +def _get_json_rpc(workspace: str) -> Union[JsonRpc, None]: + try: + return _process_manager.get_json_rpc(workspace) + except StreamClosedException: + return None + except KeyError: + return None + + +def get_or_start_json_rpc( + workspace: str, interpreter: Sequence[str], cwd: str +) -> Union[JsonRpc, None]: + """Gets an existing JSON-RPC connection or starts one and return it.""" + res = _get_json_rpc(workspace) + if not res: + args = [*interpreter, RUNNER_SCRIPT] + _process_manager.start_process(workspace, args, cwd) + res = _get_json_rpc(workspace) + return res + + +class RpcRunResult: + """Object to hold result from running tool over RPC.""" + + def __init__(self, stdout: str, stderr: str, exception: Optional[str] = None): + self.stdout: str = stdout + self.stderr: str = stderr + self.exception: Optional[str] = exception + + +# pylint: disable=too-many-arguments +def run_over_json_rpc( + workspace: str, + interpreter: Sequence[str], + module: str, + argv: Sequence[str], + use_stdin: bool, + cwd: str, + source: Optional[str] = None, +) -> RpcRunResult: + """Uses JSON-RPC to execute a command.""" + rpc: Union[JsonRpc, None] = get_or_start_json_rpc(workspace, interpreter, cwd) + if not rpc: + # pylint: disable=broad-exception-raised + raise Exception("Failed to run over JSON-RPC.") + + msg_id = str(uuid.uuid4()) + msg = { + "id": msg_id, + "method": "run", + "module": module, + "argv": argv, + "useStdin": use_stdin, + "cwd": cwd, + } + if source: + msg["source"] = source + + rpc.send_data(msg) + + data = rpc.receive_data() + + if data["id"] != msg_id: + return RpcRunResult("", f"Invalid result for request: {json.dumps(msg, indent=4)}") + + result = data["result"] if "result" in data else "" + if "error" in data: + error = data["error"] + + if data.get("exception", False): + return RpcRunResult(result, "", error) + return RpcRunResult(result, error) + + return RpcRunResult(result, "") + + +def shutdown_json_rpc(): + """Shutdown all JSON-RPC processes.""" + _process_manager.stop_all_processes() diff --git a/bundled/tool/lsp_runner.py b/bundled/tool/lsp_runner.py new file mode 100644 index 00000000..50b12f09 --- /dev/null +++ b/bundled/tool/lsp_runner.py @@ -0,0 +1,88 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +""" +Runner to use when running under a different interpreter. +""" + +import os +import pathlib +import sys +import traceback + + +# ********************************************************** +# Update sys.path before importing any bundled libraries. +# ********************************************************** +def update_sys_path(path_to_add: str, strategy: str) -> None: + """Add given path to `sys.path`.""" + if path_to_add not in sys.path and os.path.isdir(path_to_add): + if strategy == "useBundled": + sys.path.insert(0, path_to_add) + elif strategy == "fromEnvironment": + sys.path.append(path_to_add) + + +# Ensure that we can import LSP libraries, and other bundled libraries. +update_sys_path( + os.fspath(pathlib.Path(__file__).parent.parent / "libs"), + os.getenv("LS_IMPORT_STRATEGY", "useBundled"), +) + + +# pylint: disable=wrong-import-position,import-error +import lsp_jsonrpc as jsonrpc # noqa: E402 +import lsp_utils as utils # noqa: E402 + +RPC = jsonrpc.create_json_rpc(sys.stdin.buffer, sys.stdout.buffer) + +EXIT_NOW = False +while not EXIT_NOW: + msg = RPC.receive_data() + + method = msg["method"] + if method == "exit": + EXIT_NOW = True + continue + + if method == "run": + is_exception = False + # This is needed to preserve sys.path, pylint modifies + # sys.path and that might not work for this scenario + # next time around. + with utils.substitute_attr(sys, "path", sys.path[:]): + try: + # TODO: `utils.run_module` is equivalent to running `python -m `. + # If your tool supports a programmatic API then replace the function below + # with code for your tool. You can also use `utils.run_api` helper, which + # handles changing working directories, managing io streams, etc. + # Also update `_run_tool_on_document` and `_run_tool` functions in `lsp_server.py`. + result = utils.run_module( + module=msg["module"], + argv=msg["argv"], + use_stdin=msg["useStdin"], + cwd=msg["cwd"], + source=msg["source"] if "source" in msg else None, + ) + except Exception: # pylint: disable=broad-except + result = utils.RunResult("", traceback.format_exc(chain=True)) + is_exception = True + + response = {"id": msg["id"]} + if result.stderr: + response["error"] = result.stderr + response["exception"] = is_exception + elif result.stdout: + response["result"] = result.stdout + + RPC.send_data(response) diff --git a/bundled/tool/lsp_server.py b/bundled/tool/lsp_server.py new file mode 100644 index 00000000..6f6b30d8 --- /dev/null +++ b/bundled/tool/lsp_server.py @@ -0,0 +1,214 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. +"""Implementation of tool support over LSP.""" +from __future__ import annotations + +import json +import os +import pathlib +import sys +from typing import Any, Dict, List, Optional, Tuple + +from constants import TOOL_DISPLAY_NAME, TOOL_MODULE_NAME, ZENML_CLIENT_INITIALIZED + + +# ********************************************************** +# Update sys.path before importing any bundled libraries. +# ********************************************************** +def update_sys_path(path_to_add: str, strategy: str) -> None: + """Add given path to `sys.path`.""" + if path_to_add not in sys.path and os.path.isdir(path_to_add): + if strategy == "useBundled": + sys.path.insert(0, path_to_add) + elif strategy == "fromEnvironment": + sys.path.append(path_to_add) + + +# Ensure that we can import LSP libraries, and other bundled libraries. +update_sys_path( + os.fspath(pathlib.Path(__file__).parent.parent / "libs"), + os.getenv("LS_IMPORT_STRATEGY", "useBundled"), +) + + +# ********************************************************** +# Imports needed for the language server goes below this. +# ********************************************************** +# pylint: disable=wrong-import-position,import-error +import lsp_jsonrpc as jsonrpc # noqa: E402 +import lsprotocol.types as lsp # noqa: E402 +from lsp_zenml import ZenLanguageServer # noqa: E402 +from pygls import uris, workspace # noqa: E402 + +WORKSPACE_SETTINGS: Dict[str, Any] = {} +GLOBAL_SETTINGS: Dict[str, Any] = {} +RUNNER = pathlib.Path(__file__).parent / "lsp_runner.py" + +MAX_WORKERS = 5 + +LSP_SERVER = ZenLanguageServer(name="zen-language-server", version="0.0.1", max_workers=MAX_WORKERS) + +# ********************************************************** +# Tool specific code goes below this. +# ********************************************************** +TOOL_MODULE = TOOL_MODULE_NAME +TOOL_DISPLAY = TOOL_DISPLAY_NAME +# Default arguments always passed to zenml. (Not currently used) +TOOL_ARGS: List[str] = [] +# Versions of zenml found by workspace +VERSION_LOOKUP: Dict[str, Tuple[int, int, int]] = {} + + +# ********************************************************** +# Required Language Server Initialization and Exit handlers. +# ********************************************************** +@LSP_SERVER.feature(lsp.INITIALIZE) +async def initialize(params: lsp.InitializeParams) -> None: + """LSP handler for initialize request.""" + # pylint: disable=global-statement + log_to_output(f"CWD Server: {os.getcwd()}") + + paths = "\r\n ".join(sys.path) + log_to_output(f"sys.path used to run Server:\r\n {paths}") + + # Check if initialization_options is a dictionary and update GLOBAL_SETTINGS safely + if isinstance(params.initialization_options, dict): + global_settings = params.initialization_options.get("globalSettings", {}) + if isinstance(global_settings, dict): + GLOBAL_SETTINGS.update(**global_settings) + + # Safely access 'settings' from initialization_options if present + settings = params.initialization_options.get("settings") + if settings is not None: + _update_workspace_settings(settings) + log_to_output( + f"Settings used to run Server:\r\n{json.dumps(settings, indent=4, ensure_ascii=False)}\r\n" + ) + + log_to_output( + f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n" + ) + log_to_output( + f"Workspace settings:\r\n{json.dumps(WORKSPACE_SETTINGS, indent=4, ensure_ascii=False)}\r\n" + ) + + log_to_output("ZenML LSP is initializing.") + LSP_SERVER.send_custom_notification("sanityCheck", "ZenML LSP is initializing.") + + # Below is not needed as the interpreter path gets automatically updated when changed in vscode. + # interpreter_path = WORKSPACE_SETTINGS[os.getcwd()]["interpreter"][0] + # LSP_SERVER.update_python_interpreter(interpreter_path) + + # Check install status and initialize ZenML client if ZenML is installed. + await LSP_SERVER.initialize_zenml_client() + + # Wait for 5 secondsto allow the language client to setup and settle down client side. + ready_status = {"ready": True} if LSP_SERVER.zenml_client else {"ready": False} + LSP_SERVER.send_custom_notification(ZENML_CLIENT_INITIALIZED, ready_status) + + +@LSP_SERVER.feature(lsp.EXIT) +def on_exit(_params: Optional[Any] = None) -> None: + """Handle clean up on exit.""" + jsonrpc.shutdown_json_rpc() + + +@LSP_SERVER.feature(lsp.SHUTDOWN) +def on_shutdown(_params: Optional[Any] = None) -> None: + """Handle clean up on shutdown.""" + jsonrpc.shutdown_json_rpc() + + +# ***************************************************** +# Internal functional and settings management APIs. +# ***************************************************** +def _get_global_defaults(): + return { + "path": GLOBAL_SETTINGS.get("path", []), + "interpreter": GLOBAL_SETTINGS.get("interpreter", [sys.executable]), + "args": GLOBAL_SETTINGS.get("args", []), + "importStrategy": GLOBAL_SETTINGS.get("importStrategy", "useBundled"), + "showNotifications": GLOBAL_SETTINGS.get("showNotifications", "off"), + } + + +def _update_workspace_settings(settings): + if not settings: + key = os.getcwd() + WORKSPACE_SETTINGS[key] = { + "cwd": key, + "workspaceFS": key, + "workspace": uris.from_fs_path(key), + **_get_global_defaults(), + } + return + + for setting in settings: + key = uris.to_fs_path(setting["workspace"]) + WORKSPACE_SETTINGS[key] = { + "cwd": key, + **setting, + "workspaceFS": key, + } + + +# ***************************************************** +# Internal execution APIs. +# ***************************************************** +def get_cwd(settings: Dict[str, Any], document: Optional[workspace.Document]) -> str: + """Returns cwd for the given settings and document.""" + if settings["cwd"] == "${workspaceFolder}": + return settings["workspaceFS"] + + if settings["cwd"] == "${fileDirname}": + if document is not None: + return os.fspath(pathlib.Path(document.path).parent) + return settings["workspaceFS"] + + return settings["cwd"] + + +# ***************************************************** +# Logging and notification. +# ***************************************************** +def log_to_output(message: str, msg_type: lsp.MessageType = lsp.MessageType.Log) -> None: + """Log to output.""" + LSP_SERVER.show_message_log(message, msg_type) + + +def log_error(message: str) -> None: + """Log error.""" + LSP_SERVER.show_message_log(message, lsp.MessageType.Error) + if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["onError", "onWarning", "always"]: + LSP_SERVER.show_message(message, lsp.MessageType.Error) + + +def log_warning(message: str) -> None: + """Log warning.""" + LSP_SERVER.show_message_log(message, lsp.MessageType.Warning) + if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["onWarning", "always"]: + LSP_SERVER.show_message(message, lsp.MessageType.Warning) + + +def log_always(message: str) -> None: + """Log message.""" + LSP_SERVER.show_message_log(message, lsp.MessageType.Info) + if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["always"]: + LSP_SERVER.show_message(message, lsp.MessageType.Info) + + +# ***************************************************** +# Start the server. +# ***************************************************** +if __name__ == "__main__": + LSP_SERVER.start_io() diff --git a/bundled/tool/lsp_utils.py b/bundled/tool/lsp_utils.py new file mode 100644 index 00000000..cb7b718e --- /dev/null +++ b/bundled/tool/lsp_utils.py @@ -0,0 +1,228 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +"""Utility functions and classes for use with running tools over LSP.""" +from __future__ import annotations + +import contextlib +import io +import os +import os.path +import runpy +import site +import subprocess +import sys +import threading +from typing import Any, Callable, List, Optional, Sequence, Tuple, Union + +# Save the working directory used when loading this module +SERVER_CWD = os.getcwd() +CWD_LOCK = threading.Lock() + + +def as_list(content: Union[Any, List[Any], Tuple[Any, ...]]) -> List[Any]: + """Ensures we always get a list""" + if isinstance(content, list): + return content + elif isinstance(content, tuple): + return list(content) # Convert tuple to list + return [content] + + +# pylint: disable-next=consider-using-generator +_site_paths = tuple( + [ + os.path.normcase(os.path.normpath(p)) + for p in (as_list(site.getsitepackages()) + as_list(site.getusersitepackages())) + ] +) + + +def is_same_path(file_path1, file_path2) -> bool: + """Returns true if two paths are the same.""" + return os.path.normcase(os.path.normpath(file_path1)) == os.path.normcase( + os.path.normpath(file_path2) + ) + + +def is_current_interpreter(executable) -> bool: + """Returns true if the executable path is same as the current interpreter.""" + return is_same_path(executable, sys.executable) + + +def is_stdlib_file(file_path) -> bool: + """Return True if the file belongs to standard library.""" + return os.path.normcase(os.path.normpath(file_path)).startswith(_site_paths) + + +# pylint: disable-next=too-few-public-methods +class RunResult: + """Object to hold result from running tool.""" + + def __init__(self, stdout: str, stderr: str): + self.stdout: str = stdout + self.stderr: str = stderr + + +class CustomIO(io.TextIOWrapper): + """Custom stream object to replace stdio.""" + + name = None + + def __init__(self, name, encoding="utf-8", newline=None): + self._buffer = io.BytesIO() + self._buffer.name = name + super().__init__(self._buffer, encoding=encoding, newline=newline) + + def close(self): + """Provide this close method which is used by some tools.""" + # This is intentionally empty. + + def get_value(self) -> str: + """Returns value from the buffer as string.""" + self.seek(0) + return self.read() + + +@contextlib.contextmanager +def substitute_attr(obj: Any, attribute: str, new_value: Any): + """Manage object attributes context when using runpy.run_module().""" + old_value = getattr(obj, attribute) + setattr(obj, attribute, new_value) + yield + setattr(obj, attribute, old_value) + + +@contextlib.contextmanager +def redirect_io(stream: str, new_stream): + """Redirect stdio streams to a custom stream.""" + old_stream = getattr(sys, stream) + setattr(sys, stream, new_stream) + yield + setattr(sys, stream, old_stream) + + +@contextlib.contextmanager +def change_cwd(new_cwd): + """Change working directory before running code.""" + os.chdir(new_cwd) + yield + os.chdir(SERVER_CWD) + + +def _run_module( + module: str, argv: Sequence[str], use_stdin: bool, source: Optional[str] = None +) -> RunResult: + """Runs as a module.""" + str_output = CustomIO("", encoding="utf-8") + str_error = CustomIO("", encoding="utf-8") + + try: + with substitute_attr(sys, "argv", argv): + with redirect_io("stdout", str_output): + with redirect_io("stderr", str_error): + if use_stdin and source is not None: + str_input = CustomIO("", encoding="utf-8", newline="\n") + with redirect_io("stdin", str_input): + str_input.write(source) + str_input.seek(0) + runpy.run_module(module, run_name="__main__") + else: + runpy.run_module(module, run_name="__main__") + except SystemExit: + pass + + return RunResult(str_output.get_value(), str_error.get_value()) + + +def run_module( + module: str, + argv: Sequence[str], + use_stdin: bool, + cwd: str, + source: Optional[str] = None, +) -> RunResult: + """Runs as a module.""" + with CWD_LOCK: + if is_same_path(os.getcwd(), cwd): + return _run_module(module, argv, use_stdin, source) + with change_cwd(cwd): + return _run_module(module, argv, use_stdin, source) + + +def run_path( + argv: Sequence[str], use_stdin: bool, cwd: str, source: Optional[str] = None +) -> RunResult: + """Runs as an executable.""" + if use_stdin: + with subprocess.Popen( + argv, + encoding="utf-8", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.PIPE, + cwd=cwd, + ) as process: + return RunResult(*process.communicate(input=source)) + else: + result = subprocess.run( + argv, + encoding="utf-8", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + cwd=cwd, + ) + return RunResult(result.stdout, result.stderr) + + +def run_api( + callback: Callable[[Sequence[str], CustomIO, CustomIO, CustomIO | None], None], + argv: Sequence[str], + use_stdin: bool, + cwd: str, + source: Optional[str] = None, +) -> RunResult: + """Run a API.""" + with CWD_LOCK: + if is_same_path(os.getcwd(), cwd): + return _run_api(callback, argv, use_stdin, source) + with change_cwd(cwd): + return _run_api(callback, argv, use_stdin, source) + + +def _run_api( + callback: Callable[[Sequence[str], CustomIO, CustomIO, CustomIO | None], None], + argv: Sequence[str], + use_stdin: bool, + source: Optional[str] = None, +) -> RunResult: + str_output = CustomIO("", encoding="utf-8") + str_error = CustomIO("", encoding="utf-8") + + try: + with substitute_attr(sys, "argv", argv): + with redirect_io("stdout", str_output): + with redirect_io("stderr", str_error): + if use_stdin and source is not None: + str_input = CustomIO("", encoding="utf-8", newline="\n") + with redirect_io("stdin", str_input): + str_input.write(source) + str_input.seek(0) + callback(argv, str_output, str_error, str_input) + else: + callback(argv, str_output, str_error, None) + except SystemExit: + pass + + return RunResult(str_output.get_value(), str_error.get_value()) diff --git a/bundled/tool/lsp_zenml.py b/bundled/tool/lsp_zenml.py new file mode 100644 index 00000000..e2264823 --- /dev/null +++ b/bundled/tool/lsp_zenml.py @@ -0,0 +1,369 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. +""" +Extends the main Language Server Protocol (LSP) server for ZenML +by adding custom functionalities. It acts as a wrapper around the core LSP +server implementation (`lsp_server.py`), providing ZenML-specific features +such as checking ZenML installation, verifying version compatibility, and +updating Python interpreter paths. +""" + + +import asyncio +import subprocess +import sys +from functools import wraps + +import lsprotocol.types as lsp +from constants import IS_ZENML_INSTALLED, MIN_ZENML_VERSION, TOOL_MODULE_NAME +from lazy_import import suppress_stdout_temporarily +from packaging.version import parse as parse_version +from pygls.server import LanguageServer +from zen_watcher import ZenConfigWatcher +from zenml_client import ZenMLClient + +zenml_init_error = { + "error": "ZenML is not initialized. Please check ZenML version requirements." +} + + +class ZenLanguageServer(LanguageServer): + """ZenML Language Server implementation.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.python_interpreter = sys.executable + self.zenml_client = None + # self.register_commands() + + async def is_zenml_installed(self) -> bool: + """Asynchronously checks if ZenML is installed.""" + try: + process = await asyncio.create_subprocess_exec( + self.python_interpreter, + "-c", + "import zenml; print(zenml.__version__)", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + await process.wait() + if process.returncode == 0: + self.show_message_log("✅ ZenML installation check: Successful.") + return True + self.show_message_log( + "❌ ZenML installation check failed.", lsp.MessageType.Error + ) + return False + except Exception as e: + self.show_message_log( + f"Error checking ZenML installation: {str(e)}", lsp.MessageType.Error + ) + return False + + async def initialize_zenml_client(self): + """Initializes the ZenML client.""" + self.send_custom_notification("zenml/client", {"status": "pending"}) + if self.zenml_client is not None: + # Client is already initialized. + self.notify_user("⭐️ ZenML Client Already Initialized ⭐️") + return + + if not await self.is_zenml_installed(): + self.send_custom_notification(IS_ZENML_INSTALLED, {"is_installed": False}) + self.notify_user("❗ ZenML not detected.", lsp.MessageType.Warning) + return + + zenml_version = self.get_zenml_version() + self.send_custom_notification( + IS_ZENML_INSTALLED, {"is_installed": True, "version": zenml_version} + ) + # Initializing ZenML client after successful installation check. + self.log_to_output("🚀 Initializing ZenML client...") + try: + self.zenml_client = ZenMLClient() + self.show_message_log("✅ ZenML client initialized successfully.") + # register pytool module commands + self.register_commands() + # initialize watcher + self.initialize_global_config_watcher() + except Exception as e: + self.notify_user( + f"Failed to initialize ZenML client: {str(e)}", lsp.MessageType.Error + ) + + def initialize_global_config_watcher(self): + """Sets up and starts the Global Configuration Watcher.""" + try: + watcher = ZenConfigWatcher(self) + watcher.watch_zenml_config_yaml() + self.log_to_output("👀 Watching ZenML configuration for changes.") + except Exception as e: + self.notify_user( + f"Error setting up the Global Configuration Watcher: {e}", + msg_type=lsp.MessageType.Error, + ) + + def zenml_command(self, wrapper_name=None): + """ + Decorator for executing commands with ZenMLClient or its specified wrapper. + + This decorator ensures that commands are only executed if ZenMLClient is properly + initialized. If a `wrapper_name` is provided, the command targets a specific + wrapper within ZenMLClient; otherwise, it targets ZenMLClient directly. + + Args: + wrapper_name (str, optional): The specific wrapper within ZenMLClient to target. + Defaults to None, targeting the ZenMLClient itself. + """ + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + client = self.zenml_client + if not client: + self.log_to_output("ZenML client not found in ZenLanguageServer.") + return zenml_init_error + self.log_to_output(f"Executing command with wrapper: {wrapper_name}") + if not client.initialized: + return zenml_init_error + + with suppress_stdout_temporarily(): + if wrapper_name: + wrapper_instance = getattr( + self.zenml_client, wrapper_name, None + ) + if not wrapper_instance: + return {"error": f"Wrapper '{wrapper_name}' not found."} + return func(wrapper_instance, *args, **kwargs) + return func(self.zenml_client, *args, **kwargs) + + return wrapper + + return decorator + + def get_zenml_version(self) -> str: + """Gets the ZenML version.""" + command = [ + self.python_interpreter, + "-c", + "import zenml; print(zenml.__version__)", + ] + result = subprocess.run(command, capture_output=True, text=True, check=True) + return result.stdout.strip() + + def check_zenml_version(self) -> dict: + """Checks if the installed ZenML version meets the minimum requirement.""" + version_str = self.get_zenml_version() + installed_version = parse_version(version_str) + if installed_version < parse_version(MIN_ZENML_VERSION): + return self._construct_version_validation_response(False, version_str) + + return self._construct_version_validation_response(True, version_str) + + def _construct_version_validation_response(self, meets_requirement, version_str): + """Constructs a version validation response.""" + if meets_requirement: + message = "ZenML version requirement is met." + status = {"message": message, "version": version_str, "is_valid": True} + else: + message = f"Supported versions >= {MIN_ZENML_VERSION}. Found version {version_str}." + status = {"message": message, "version": version_str, "is_valid": False} + + self.send_custom_notification("zenml/version", status) + self.notify_user(message) + return status + + def send_custom_notification(self, method: str, args: dict): + """Sends a custom notification to the LSP client.""" + self.show_message_log( + f"Sending custom notification: {method} with args: {args}" + ) + self.send_notification(method, args) + + def update_python_interpreter(self, interpreter_path): + """Updates the Python interpreter path and handles errors.""" + try: + self.python_interpreter = interpreter_path + self.show_message_log( + f"LSP_Python_Interpreter Updated: {self.python_interpreter}" + ) + # pylint: disable=broad-exception-caught + except Exception as e: + self.show_message_log( + f"Failed to update Python interpreter: {str(e)}", lsp.MessageType.Error + ) + + def notify_user( + self, message: str, msg_type: lsp.MessageType = lsp.MessageType.Info + ): + """Logs a message and also notifies the user.""" + self.show_message(message, msg_type) + + def log_to_output( + self, message: str, msg_type: lsp.MessageType = lsp.MessageType.Log + ) -> None: + """Log to output.""" + self.show_message_log(message, msg_type) + + def register_commands(self): + """Registers ZenML Python Tool commands.""" + + @self.command(f"{TOOL_MODULE_NAME}.getGlobalConfig") + @self.zenml_command(wrapper_name="config_wrapper") + def get_global_configuration(wrapper_instance, *args, **kwargs) -> dict: + """Fetches global ZenML configuration settings.""" + return wrapper_instance.get_global_configuration() + + @self.command(f"{TOOL_MODULE_NAME}.getGlobalConfigFilePath") + @self.zenml_command(wrapper_name="config_wrapper") + def get_global_config_file_path(wrapper_instance, *args, **kwargs): + """Retrieves the file path of the global ZenML configuration.""" + return wrapper_instance.get_global_config_file_path() + + @self.command(f"{TOOL_MODULE_NAME}.serverInfo") + @self.zenml_command(wrapper_name="zen_server_wrapper") + def get_server_info(wrapper_instance, *args, **kwargs): + """Gets information about the ZenML server.""" + return wrapper_instance.get_server_info() + + @self.command(f"{TOOL_MODULE_NAME}.connect") + @self.zenml_command(wrapper_name="zen_server_wrapper") + def connect(wrapper_instance, args): + """Connects to a ZenML server with specified arguments.""" + return wrapper_instance.connect(args) + + @self.command(f"{TOOL_MODULE_NAME}.disconnect") + @self.zenml_command(wrapper_name="zen_server_wrapper") + def disconnect(wrapper_instance, *args, **kwargs): + """Disconnects from the current ZenML server.""" + return wrapper_instance.disconnect(*args, **kwargs) + + @self.command(f"{TOOL_MODULE_NAME}.fetchStacks") + @self.zenml_command(wrapper_name="stacks_wrapper") + def fetch_stacks(wrapper_instance, args): + """Fetches a list of all ZenML stacks.""" + return wrapper_instance.fetch_stacks(args) + + @self.command(f"{TOOL_MODULE_NAME}.getActiveStack") + @self.zenml_command(wrapper_name="stacks_wrapper") + def get_active_stack(wrapper_instance, *args, **kwargs): + """Gets the currently active ZenML stack.""" + return wrapper_instance.get_active_stack() + + @self.command(f"{TOOL_MODULE_NAME}.switchActiveStack") + @self.zenml_command(wrapper_name="stacks_wrapper") + def set_active_stack(wrapper_instance, args): + """Sets the active ZenML stack to the specified stack.""" + return wrapper_instance.set_active_stack(args) + + @self.command(f"{TOOL_MODULE_NAME}.renameStack") + @self.zenml_command(wrapper_name="stacks_wrapper") + def rename_stack(wrapper_instance, args): + """Renames a specified ZenML stack.""" + return wrapper_instance.rename_stack(args) + + @self.command(f"{TOOL_MODULE_NAME}.copyStack") + @self.zenml_command(wrapper_name="stacks_wrapper") + def copy_stack(wrapper_instance, args): + """Copies a specified ZenML stack to a new stack.""" + return wrapper_instance.copy_stack(args) + + @self.command(f"{TOOL_MODULE_NAME}.registerStack") + @self.zenml_command(wrapper_name="stacks_wrapper") + def register_stack(wrapper_instance, args): + """Registers a new ZenML stack.""" + return wrapper_instance.register_stack(args) + + @self.command(f"{TOOL_MODULE_NAME}.updateStack") + @self.zenml_command(wrapper_name="stacks_wrapper") + def update_stack(wrapper_instance, args): + """Updates a specified ZenML stack .""" + return wrapper_instance.update_stack(args) + + @self.command(f"{TOOL_MODULE_NAME}.deleteStack") + @self.zenml_command(wrapper_name="stacks_wrapper") + def delete_stack(wrapper_instance, args): + """Deletes a specified ZenML stack .""" + return wrapper_instance.delete_stack(args) + + @self.command(f"{TOOL_MODULE_NAME}.registerComponent") + @self.zenml_command(wrapper_name="stacks_wrapper") + def register_component(wrapper_instance, args): + """Registers a Zenml stack component""" + return wrapper_instance.register_component(args) + + @self.command(f"{TOOL_MODULE_NAME}.updateComponent") + @self.zenml_command(wrapper_name="stacks_wrapper") + def update_component(wrapper_instance, args): + """Updates a ZenML stack component""" + return wrapper_instance.update_component(args) + + @self.command(f"{TOOL_MODULE_NAME}.deleteComponent") + @self.zenml_command(wrapper_name="stacks_wrapper") + def delete_component(wrapper_instance, args): + """Deletes a specified ZenML stack component""" + return wrapper_instance.delete_component(args) + + @self.command(f"{TOOL_MODULE_NAME}.listComponents") + @self.zenml_command(wrapper_name="stacks_wrapper") + def list_components(wrapper_instance, args): + """Get paginated list of stack components from ZenML""" + return wrapper_instance.list_components(args) + + @self.command(f"{TOOL_MODULE_NAME}.getComponentTypes") + @self.zenml_command(wrapper_name="stacks_wrapper") + def get_component_types(wrapper_instance, args): + """Get list of component types from ZenML""" + return wrapper_instance.get_component_types() + + @self.command(f"{TOOL_MODULE_NAME}.listFlavors") + @self.zenml_command(wrapper_name="stacks_wrapper") + def list_flavors(wrapper_instance, args): + """Get paginated list of component flavors from ZenML""" + return wrapper_instance.list_flavors(args) + + @self.command(f"{TOOL_MODULE_NAME}.getPipelineRuns") + @self.zenml_command(wrapper_name="pipeline_runs_wrapper") + def fetch_pipeline_runs(wrapper_instance, args): + """Fetches all ZenML pipeline runs.""" + return wrapper_instance.fetch_pipeline_runs(args) + + @self.command(f"{TOOL_MODULE_NAME}.deletePipelineRun") + @self.zenml_command(wrapper_name="pipeline_runs_wrapper") + def delete_pipeline_run(wrapper_instance, args): + """Deletes a specified ZenML pipeline run.""" + return wrapper_instance.delete_pipeline_run(args) + + @self.command(f"{TOOL_MODULE_NAME}.getPipelineRun") + @self.zenml_command(wrapper_name="pipeline_runs_wrapper") + def get_pipeline_run(wrapper_instance, args): + """Gets a specified ZenML pipeline run.""" + return wrapper_instance.get_pipeline_run(args) + + @self.command(f"{TOOL_MODULE_NAME}.getPipelineRunStep") + @self.zenml_command(wrapper_name="pipeline_runs_wrapper") + def get_run_step(wrapper_instance, args): + """Gets a specified ZenML pipeline run step.""" + return wrapper_instance.get_run_step(args) + + @self.command(f"{TOOL_MODULE_NAME}.getPipelineRunArtifact") + @self.zenml_command(wrapper_name="pipeline_runs_wrapper") + def get_run_artifact(wrapper_instance, args): + """Gets a specified ZenML pipeline artifact""" + return wrapper_instance.get_run_artifact(args) + + @self.command(f"{TOOL_MODULE_NAME}.getPipelineRunDag") + @self.zenml_command(wrapper_name="pipeline_runs_wrapper") + def get_run_dag(wrapper_instance, args): + """Gets graph data for a specified ZenML pipeline run""" + return wrapper_instance.get_pipeline_run_graph(args) diff --git a/bundled/tool/type_hints.py b/bundled/tool/type_hints.py new file mode 100644 index 00000000..ce524886 --- /dev/null +++ b/bundled/tool/type_hints.py @@ -0,0 +1,130 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. +from typing import Any, TypedDict, Dict, List, Optional +from uuid import UUID + + +class StepArtifactBody(TypedDict): + type: str + artifact: Dict[str, str] + +class StepArtifact(TypedDict): + id: UUID + body: StepArtifactBody + +class GraphNode(TypedDict): + id: str + type: str + data: Dict[str, str] + +class GraphEdge(TypedDict): + id: str + source: str + target: str + +class GraphResponse(TypedDict): + nodes: List[GraphNode] + edges: List[GraphEdge] + name: str + status: str + version: str + +class ErrorResponse(TypedDict): + error: str + +class RunStepResponse(TypedDict): + name: str + id: str + status: str + author: Dict[str, str] + startTime: Optional[str] + endTime: Optional[str] + duration: Optional[str] + stackName: str + orchestrator: Dict[str, str] + pipeline: Dict[str, str] + cacheKey: str + sourceCode: str + logsUri: str + +class RunArtifactResponse(TypedDict): + name: str + version: str + id: str + type: str + author: Dict[str, str] + update: str + data: Dict[str, str] + metadata: Dict[str, Any] + +class ZenmlStoreInfo(TypedDict): + id: str + version: str + debug: bool + deployment_type: str + database_type: str + secrets_store_type: str + auth_scheme: str + server_url: str + dashboard_url: str + +class ZenmlStoreConfig(TypedDict): + type: str + url: str + api_token: Optional[str] + +class ZenmlServerInfoResp(TypedDict): + store_info: ZenmlStoreInfo + store_config: ZenmlStoreConfig + +class ZenmlGlobalConfigResp(TypedDict): + user_id: str + user_email: str + analytics_opt_in: bool + version: str + active_stack_id: str + active_workspace_name: str + store: ZenmlStoreConfig + +class StackComponent(TypedDict): + id: str + name: str + flavor: str + type: str + config: Dict[str, Any] + +class ListComponentsResponse(TypedDict): + index: int + max_size: int + total_pages: int + total: int + items: List[StackComponent] + +class Flavor(TypedDict): + id: str + name: str + type: str + logo_url: str + config_schema: Dict[str, Any] + docs_url: Optional[str] + sdk_docs_url: Optional[str] + connector_type: Optional[str] + connector_resource_type: Optional[str] + connector_resource_id_attr: Optional[str] + +class ListFlavorsResponse(TypedDict): + index: int + max_size: int + total_pages: int + total: int + items: List[Flavor] \ No newline at end of file diff --git a/bundled/tool/zen_watcher.py b/bundled/tool/zen_watcher.py new file mode 100644 index 00000000..71d18022 --- /dev/null +++ b/bundled/tool/zen_watcher.py @@ -0,0 +1,147 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. +""" +ZenML Global Configuration Watcher. + +This module contains ZenConfigWatcher, a class that watches for changes +in the ZenML global configuration file and triggers notifications accordingly. +""" +import os +from threading import Timer +from typing import Any, Optional + +import yaml +from constants import ZENML_SERVER_CHANGED, ZENML_STACK_CHANGED +from lazy_import import suppress_stdout_temporarily +from watchdog.events import FileSystemEventHandler +from watchdog.observers import Observer + + +class ZenConfigWatcher(FileSystemEventHandler): + """ + Watches for changes in the ZenML global configuration file. + + Upon modification of the global configuration file, it triggers notifications + to update config details accordingly. + """ + + def __init__(self, lsp_server): + super().__init__() + self.LSP_SERVER = lsp_server + self.observer: Optional[Any] = None + self.debounce_interval: float = 2.0 + self._timer: Optional[Timer] = None + self.last_known_url: str = "" + self.last_known_stack_id: str = "" + self.show_notification: bool = os.getenv("LS_SHOW_NOTIFICATION", "off") in [ + "onError", + "onWarning", + "always", + ] + + try: + with suppress_stdout_temporarily(): + config_wrapper_instance = self.LSP_SERVER.zenml_client.config_wrapper + self.config_path = config_wrapper_instance.get_global_config_file_path() + except Exception as e: + self.log_error(f"Failed to retrieve global config file path: {e}") + + + def process_config_change(self, config_file_path: str): + """Process the configuration file change.""" + with suppress_stdout_temporarily(): + try: + with open(config_file_path, "r") as f: + config = yaml.safe_load(f) + + new_url = config.get("store", {}).get("url", "") + new_stack_id = config.get("active_stack_id", "") + + url_changed = new_url != self.last_known_url + stack_id_changed = new_stack_id != self.last_known_stack_id + # Send ZENML_SERVER_CHANGED if url changed + if url_changed: + server_details = { + "url": new_url, + "api_token": config.get("store", {}).get("api_token", ""), + "store_type": config.get("store", {}).get("type", ""), + } + self.LSP_SERVER.send_custom_notification( + ZENML_SERVER_CHANGED, + server_details, + ) + self.last_known_url = new_url + # Send ZENML_STACK_CHANGED if stack_id changed + if stack_id_changed: + self.LSP_SERVER.send_custom_notification(ZENML_STACK_CHANGED, new_stack_id) + self.last_known_stack_id = new_stack_id + except (FileNotFoundError, PermissionError) as e: + self.log_error(f"Configuration file access error: {e} - {config_file_path}") + except yaml.YAMLError as e: + self.log_error(f"YAML parsing error in configuration: {e} - {config_file_path}") + except Exception as e: + self.log_error(f"Unexpected error while monitoring configuration: {e}") + + def on_modified(self, event): + """ + Handles the modification event triggered when the global configuration file is changed. + """ + if event.src_path != self.config_path: + return + + if self._timer is not None: + self._timer.cancel() + + self._timer = Timer(self.debounce_interval, self.process_event, [event]) + self._timer.start() + + def process_event(self, event): + """ + Processes the event with a debounce mechanism. + """ + self.process_config_change(event.src_path) + + def watch_zenml_config_yaml(self): + """ + Initializes and starts a file watcher on the ZenML global configuration directory. + Upon detecting a change, it triggers handlers to process these changes. + """ + config_wrapper_instance = self.LSP_SERVER.zenml_client.config_wrapper + config_dir_path = config_wrapper_instance.get_global_config_directory_path() + + # Check if config_dir_path is valid and readable + if os.path.isdir(config_dir_path) and os.access(config_dir_path, os.R_OK): + try: + self.observer = Observer() + self.observer.schedule(self, config_dir_path, recursive=False) + self.observer.start() + self.LSP_SERVER.log_to_output(f"Started watching {config_dir_path} for changes.") + except Exception as e: + self.log_error(f"Failed to start file watcher: {e}") + else: + self.log_error("Config directory path invalid or missing.") + + def stop_watching(self): + """ + Stops the file watcher gracefully. + """ + if self.observer is not None: + self.observer.stop() + self.observer.join() # waits for observer to fully stop + self.LSP_SERVER.log_to_output("Stopped watching config directory for changes.") + + def log_error(self, message: str): + """Log error.""" + self.LSP_SERVER.show_message_log(message, 1) + if self.show_notification: + self.LSP_SERVER.show_message(message, 1) diff --git a/bundled/tool/zenml_client.py b/bundled/tool/zenml_client.py new file mode 100644 index 00000000..7b3111a4 --- /dev/null +++ b/bundled/tool/zenml_client.py @@ -0,0 +1,38 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressc +# or implied. See the License for the specific language governing +# permissions and limitations under the License. +"""ZenML client class. Initializes all wrappers.""" + +class ZenMLClient: + """Provides a high-level interface to ZenML functionalities by wrapping core components.""" + + def __init__(self): + """ + Initializes the ZenMLClient with wrappers for managing configurations, + server interactions, stacks, and pipeline runs. + """ + # pylint: disable=wrong-import-position,import-error + from lazy_import import lazy_import + from zenml_wrappers import ( + GlobalConfigWrapper, + PipelineRunsWrapper, + StacksWrapper, + ZenServerWrapper, + ) + + self.client = lazy_import("zenml.client", "Client")() + # initialize zenml library wrappers + self.config_wrapper = GlobalConfigWrapper() + self.zen_server_wrapper = ZenServerWrapper(self.config_wrapper) + self.stacks_wrapper = StacksWrapper(self.client) + self.pipeline_runs_wrapper = PipelineRunsWrapper(self.client) + self.initialized = True \ No newline at end of file diff --git a/bundled/tool/zenml_grapher.py b/bundled/tool/zenml_grapher.py new file mode 100644 index 00000000..f11dea18 --- /dev/null +++ b/bundled/tool/zenml_grapher.py @@ -0,0 +1,99 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. +"""This module contains a tool to mimic LineageGraph output for pipeline runs""" + +from typing import Dict, List +from type_hints import GraphEdge, GraphNode, GraphResponse, StepArtifact + +class Grapher: + """Quick and dirty implementation of ZenML/LineageGraph to reduce number of api calls""" + + def __init__(self, run): + self.run = run + self.nodes: List[GraphNode] = [] + self.edges: List[GraphEdge] = [] + self.artifacts: Dict[str, bool] = {} + + def build_nodes_from_steps(self) -> None: + """Builds internal node list from run steps""" + self.nodes = [] + self.artifacts = {} + + for step in self.run.metadata.steps: + step_data = self.run.metadata.steps[step] + self.nodes.append({ + "id": str(step_data.id), + "type": "step", + "data": { + "execution_id": str(step_data.id), + "name": step, + "status": step_data.body.status, + }, + }) + self.add_artifacts_from_list(step_data.body.inputs) + self.add_artifacts_from_list(step_data.body.outputs) + + + def add_artifacts_from_list(self, dictOfArtifacts: Dict[str, StepArtifact]) -> None: + """Used to add unique artifacts to the internal nodes list by build_nodes_from_steps""" + for artifact in dictOfArtifacts: + id = str(dictOfArtifacts[artifact].body.artifact.id) + if id in self.artifacts: + continue + + self.artifacts[id] = True + + self.nodes.append({ + "type": "artifact", + "id": id, + "data": { + "name": artifact, + "artifact_type": dictOfArtifacts[artifact].body.type, + "execution_id": str(dictOfArtifacts[artifact].id), + }, + }) + + + def build_edges_from_steps(self) -> None: + """Builds internal edges list from run steps""" + self.edges = [] + + for step in self.run.metadata.steps: + step_data = self.run.metadata.steps[step] + step_id = str(step_data.id) + + for artifact in step_data.body.inputs: + input_id = str(step_data.body.inputs[artifact].body.artifact.id) + self.add_edge(input_id, step_id) + + for artifact in step_data.body.outputs: + output_id = str(step_data.body.outputs[artifact].body.artifact.id) + self.add_edge(step_id, output_id) + + + def add_edge(self, v: str, w: str) -> None: + """Helper method to add an edge to the internal edges list""" + self.edges.append({ + "id": f"{v}_{w}", + "source": v, + "target": w, + }) + + def to_dict(self) -> GraphResponse: + """Returns dictionary containing graph data""" + return { + "nodes": self.nodes, + "edges": self.edges, + "status": self.run.body.status, + "name": self.run.body.pipeline.name, + } \ No newline at end of file diff --git a/bundled/tool/zenml_wrappers.py b/bundled/tool/zenml_wrappers.py new file mode 100644 index 00000000..055e2037 --- /dev/null +++ b/bundled/tool/zenml_wrappers.py @@ -0,0 +1,928 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. +"""This module provides wrappers for ZenML configuration and operations.""" + +import pathlib +from typing import Any, Tuple, Union, List, Optional, Dict +from zenml_grapher import Grapher +from type_hints import ( + GraphResponse, + ErrorResponse, + RunStepResponse, + RunArtifactResponse, + ZenmlServerInfoResp, + ZenmlGlobalConfigResp, + ListComponentsResponse, + ListFlavorsResponse +) + + +class GlobalConfigWrapper: + """Wrapper class for global configuration management.""" + + def __init__(self): + # pylint: disable=wrong-import-position,import-error + from lazy_import import lazy_import + + self.lazy_import = lazy_import + """Initializes the GlobalConfigWrapper instance.""" + self._gc = lazy_import("zenml.config.global_config", "GlobalConfiguration")() + + @property + def gc(self): + """Returns the global configuration instance.""" + return self._gc + + @property + def fileio(self): + """Provides access to file I/O operations.""" + return self.lazy_import("zenml.io", "fileio") + + @property + def get_global_config_directory(self): + """Returns the function to get the global configuration directory.""" + return self.lazy_import("zenml.utils.io_utils", "get_global_config_directory") + + @property + def RestZenStoreConfiguration(self): + """Returns the RestZenStoreConfiguration class for store configuration.""" + # pylint: disable=not-callable + return self.lazy_import( + "zenml.zen_stores.rest_zen_store", "RestZenStoreConfiguration" + ) + + def get_global_config_directory_path(self) -> str: + """Get the global configuration directory path. + + Returns: + str: Path to the global configuration directory. + """ + # pylint: disable=not-callable + config_dir = pathlib.Path(self.get_global_config_directory()) + if self.fileio.exists(str(config_dir)): + return str(config_dir) + return "Configuration directory does not exist." + + def get_global_config_file_path(self) -> str: + """Get the global configuration file path. + + Returns: + str: Path to the global configuration file. + """ + # pylint: disable=not-callable + config_dir = pathlib.Path(self.get_global_config_directory()) + config_path = config_dir / "config.yaml" + if self.fileio.exists(str(config_path)): + return str(config_path) + return "Configuration file does not exist." + + def set_store_configuration(self, remote_url: str, access_token: str): + """Set the store configuration. + + Args: + remote_url (str): Remote URL. + access_token (str): Access token. + """ + # pylint: disable=not-callable + new_store_config = self.RestZenStoreConfiguration( + type="rest", url=remote_url, api_token=access_token, verify_ssl=True + ) + + # Method name changed in 0.55.4 - 0.56.1 + if hasattr(self.gc, "set_store_configuration"): + self.gc.set_store_configuration(new_store_config) + elif hasattr(self.gc, "set_store"): + self.gc.set_store(new_store_config) + else: + raise AttributeError( + "GlobalConfiguration object does not have a method to set store configuration." + ) + self.gc.set_store(new_store_config) + + def get_global_configuration(self) -> ZenmlGlobalConfigResp: + """Get the global configuration. + + Returns: + dict: Global configuration. + """ + + store_attr_name = ( + "store_configuration" if hasattr(self.gc, "store_configuration") else "store" + ) + + store_data = getattr(self.gc, store_attr_name) + + return { + "user_id": str(self.gc.user_id), + "user_email": self.gc.user_email, + "analytics_opt_in": self.gc.analytics_opt_in, + "version": self.gc.version, + "active_stack_id": str(self.gc.active_stack_id), + "active_workspace_name": self.gc.active_workspace_name, + "store": { + "type": store_data.type, + "url": store_data.url, + "api_token": store_data.api_token if hasattr(store_data, "api_token") else None + } + } + + +class ZenServerWrapper: + """Wrapper class for Zen Server management.""" + + def __init__(self, config_wrapper): + """Initializes ZenServerWrapper with a configuration wrapper.""" + # pylint: disable=wrong-import-position,import-error + from lazy_import import lazy_import + + self.lazy_import = lazy_import + self._config_wrapper = config_wrapper + + @property + def gc(self): + """Returns the global configuration via the config wrapper.""" + return self._config_wrapper.gc + + @property + def web_login(self): + """Provides access to the ZenML web login function.""" + return self.lazy_import("zenml.cli", "web_login") + + @property + def ServerDeploymentNotFoundError(self): + """Returns the ZenML ServerDeploymentNotFoundError class.""" + return self.lazy_import( + "zenml.zen_server.deploy.exceptions", "ServerDeploymentNotFoundError" + ) + + @property + def AuthorizationException(self): + """Returns the ZenML AuthorizationException class.""" + return self.lazy_import("zenml.exceptions", "AuthorizationException") + + @property + def StoreType(self): + """Returns the ZenML StoreType enum.""" + return self.lazy_import("zenml.enums", "StoreType") + + @property + def BaseZenStore(self): + """Returns the BaseZenStore class for ZenML store operations.""" + return self.lazy_import("zenml.zen_stores.base_zen_store", "BaseZenStore") + + @property + def ServerDeployer(self): + """Provides access to the ZenML server deployment utilities.""" + return self.lazy_import("zenml.zen_server.deploy.deployer", "ServerDeployer") + + @property + def get_active_deployment(self): + """Returns the function to get the active ZenML server deployment.""" + return self.lazy_import("zenml.zen_server.utils", "get_active_deployment") + + def get_server_info(self) -> ZenmlServerInfoResp: + """Fetches the ZenML server info. + + Returns: + dict: Dictionary containing server info. + """ + store_info = self.gc.zen_store.get_store_info() + + # Handle both 'store' and 'store_configuration' depending on version + store_attr_name = ( + "store_configuration" + if hasattr(self.gc, "store_configuration") + else "store" + ) + store_config = getattr(self.gc, store_attr_name) + + return { + "storeInfo": { + "id": str(store_info.id), + "version": store_info.version, + "debug": store_info.debug, + "deployment_type": store_info.deployment_type, + "database_type": store_info.database_type, + "secrets_store_type": store_info.secrets_store_type, + "auth_scheme": store_info.auth_scheme, + "server_url": store_info.server_url, + "dashboard_url": store_info.dashboard_url, + }, + "storeConfig": { + "type": store_config.type, + "url": store_config.url, + "api_token": store_config.api_token if hasattr(store_config, "api_token") else None + } + } + + def connect(self, args, **kwargs) -> dict: + """Connects to a ZenML server. + + Args: + args (list): List of arguments. + Returns: + dict: Dictionary containing the result of the operation. + """ + url = args[0] + verify_ssl = args[1] if len(args) > 1 else True + + if not url: + return {"error": "Server URL is required."} + + try: + # pylint: disable=not-callable + access_token = self.web_login(url=url, verify_ssl=verify_ssl) + self._config_wrapper.set_store_configuration( + remote_url=url, access_token=access_token + ) + return {"message": "Connected successfully.", "access_token": access_token} + except self.AuthorizationException as e: + return {"error": f"Authorization failed: {str(e)}"} + + def disconnect(self, args) -> dict: + """Disconnects from a ZenML server. + + Args: + args (list): List of arguments. + Returns: + dict: Dictionary containing the result of the operation. + """ + try: + # Adjust for changes from 'store' to 'store_configuration' + store_attr_name = ( + "store_configuration" + if hasattr(self.gc, "store_configuration") + else "store" + ) + url = getattr(self.gc, store_attr_name).url + store_type = self.BaseZenStore.get_store_type(url) + + # pylint: disable=not-callable + server = self.get_active_deployment(local=True) + deployer = self.ServerDeployer() + + messages = [] + + if server: + deployer.remove_server(server.config.name) + messages.append("Shut down the local ZenML server.") + else: + messages.append("No local ZenML server was found running.") + + if store_type == self.StoreType.REST: + deployer.disconnect_from_server() + messages.append("Disconnected from the remote ZenML REST server.") + + self.gc.set_default_store() + + return {"message": " ".join(messages)} + except self.ServerDeploymentNotFoundError as e: + return {"error": f"Failed to disconnect: {str(e)}"} + + +class PipelineRunsWrapper: + """Wrapper for interacting with ZenML pipeline runs.""" + + def __init__(self, client): + """Initializes PipelineRunsWrapper with a ZenML client.""" + # pylint: disable=wrong-import-position,import-error + from lazy_import import lazy_import + + self.lazy_import = lazy_import + self.client = client + + @property + def ValidationError(self): + """Returns the ZenML ZenMLBaseException class.""" + return self.lazy_import("zenml.exceptions", "ValidationError") + + @property + def ZenMLBaseException(self): + """Returns the ZenML ZenMLBaseException class.""" + return self.lazy_import("zenml.exceptions", "ZenMLBaseException") + + def fetch_pipeline_runs(self, args): + """Fetches all ZenML pipeline runs. + + Returns: + list: List of dictionaries containing pipeline run data. + """ + page = args[0] + max_size = args[1] + try: + runs_page = self.client.list_pipeline_runs( + sort_by="desc:updated", page=page, size=max_size, hydrate=True + ) + runs_data = [ + { + "id": str(run.id), + "name": run.body.pipeline.name, + "status": run.body.status, + "stackName": run.body.stack.name, + "startTime": ( + run.metadata.start_time.isoformat() + if run.metadata.start_time + else None + ), + "endTime": ( + run.metadata.end_time.isoformat() + if run.metadata.end_time + else None + ), + "os": run.metadata.client_environment.get("os", "Unknown OS"), + "osVersion": run.metadata.client_environment.get( + "os_version", + run.metadata.client_environment.get( + "mac_version", "Unknown Version" + ), + ), + "pythonVersion": run.metadata.client_environment.get( + "python_version", "Unknown" + ), + } + for run in runs_page.items + ] + + return { + "runs": runs_data, + "total": runs_page.total, + "total_pages": runs_page.total_pages, + "current_page": page, + "items_per_page": max_size, + } + except self.ValidationError as e: + return {"error": "ValidationError", "message": str(e)} + except self.ZenMLBaseException as e: + return [{"error": f"Failed to retrieve pipeline runs: {str(e)}"}] + + def delete_pipeline_run(self, args) -> dict: + """Deletes a ZenML pipeline run. + + Args: + args (list): List of arguments. + Returns: + dict: Dictionary containing the result of the operation. + """ + try: + run_id = args[0] + self.client.delete_pipeline_run(run_id) + return {"message": f"Pipeline run `{run_id}` deleted successfully."} + except self.ZenMLBaseException as e: + return {"error": f"Failed to delete pipeline run: {str(e)}"} + + def get_pipeline_run(self, args: Tuple[str]) -> dict: + """Gets a ZenML pipeline run. + + Args: + args (list): List of arguments. + Returns: + dict: Dictionary containing the result of the operation. + """ + try: + run_id = args[0] + run = self.client.get_pipeline_run(run_id, hydrate=True) + run_data = { + "id": str(run.id), + "name": run.body.pipeline.name, + "status": run.body.status, + "stackName": run.body.stack.name, + "startTime": ( + run.metadata.start_time.isoformat() + if run.metadata.start_time + else None + ), + "endTime": ( + run.metadata.end_time.isoformat() if run.metadata.end_time else None + ), + "os": run.metadata.client_environment.get("os", "Unknown OS"), + "osVersion": run.metadata.client_environment.get( + "os_version", + run.metadata.client_environment.get( + "mac_version", "Unknown Version" + ), + ), + "pythonVersion": run.metadata.client_environment.get( + "python_version", "Unknown" + ), + } + + return run_data + except self.ZenMLBaseException as e: + return {"error": f"Failed to retrieve pipeline run: {str(e)}"} + + def get_pipeline_run_graph( + self, args: Tuple[str] + ) -> Union[GraphResponse, ErrorResponse]: + """Gets a ZenML pipeline run step DAG. + + Args: + args (list): List of arguments. + Returns: + dict: Dictionary containing the result of the operation. + """ + try: + run_id = args[0] + run = self.client.get_pipeline_run(run_id) + graph = Grapher(run) + graph.build_nodes_from_steps() + graph.build_edges_from_steps() + return graph.to_dict() + except self.ZenMLBaseException as e: + return {"error": f"Failed to retrieve pipeline run graph: {str(e)}"} + + def get_run_step(self, args: Tuple[str]) -> Union[RunStepResponse, ErrorResponse]: + """Gets a ZenML pipeline run step. + + Args: + args (list): List of arguments. + Returns: + dict: Dictionary containing the result of the operation. + """ + try: + step_run_id = args[0] + step = self.client.get_run_step(step_run_id, hydrate=True) + run = self.client.get_pipeline_run( + step.metadata.pipeline_run_id, hydrate=True + ) + + step_data = { + "name": step.name, + "id": str(step.id), + "status": step.body.status, + "author": { + "fullName": step.body.user.body.full_name, + "email": step.body.user.name, + }, + "startTime": ( + step.metadata.start_time.isoformat() + if step.metadata.start_time + else None + ), + "endTime": ( + step.metadata.end_time.isoformat() + if step.metadata.end_time + else None + ), + "duration": ( + str(step.metadata.end_time - step.metadata.start_time) + if step.metadata.end_time and step.metadata.start_time + else None + ), + "stackName": run.body.stack.name, + "orchestrator": {"runId": str(run.metadata.orchestrator_run_id)}, + "pipeline": { + "name": run.body.pipeline.name, + "status": run.body.status, + }, + "cacheKey": step.metadata.cache_key, + "sourceCode": step.metadata.source_code, + "logsUri": step.metadata.logs.body.uri, + } + return step_data + except self.ZenMLBaseException as e: + return {"error": f"Failed to retrieve pipeline run step: {str(e)}"} + + def get_run_artifact( + self, args: Tuple[str] + ) -> Union[RunArtifactResponse, ErrorResponse]: + """Gets a ZenML pipeline run artifact. + + Args: + args (list): List of arguments. + Returns: + dict: Dictionary containing the result of the operation. + """ + try: + artifact_id = args[0] + artifact = self.client.get_artifact_version(artifact_id, hydrate=True) + + metadata = {} + for key in artifact.metadata.run_metadata: + metadata[key] = artifact.metadata.run_metadata[key].body.value + + artifact_data = { + "name": artifact.body.artifact.name, + "version": artifact.body.version, + "id": str(artifact.id), + "type": artifact.body.type, + "author": { + "fullName": artifact.body.user.body.full_name, + "email": artifact.body.user.name, + }, + "updated": artifact.body.updated.isoformat(), + "data": { + "uri": artifact.body.uri, + "dataType": artifact.body.data_type.attribute, + }, + "metadata": metadata, + } + return artifact_data + + except self.ZenMLBaseException as e: + return {"error": f"Failed to retrieve pipeline run artifact: {str(e)}"} + + +class StacksWrapper: + """Wrapper class for Stacks management.""" + + def __init__(self, client): + """Initializes StacksWrapper with a ZenML client.""" + # pylint: disable=wrong-import-position,import-error + from lazy_import import lazy_import + + self.lazy_import = lazy_import + self.client = client + + @property + def ZenMLBaseException(self): + """Returns the ZenML ZenMLBaseException class.""" + return self.lazy_import("zenml.exceptions", "ZenMLBaseException") + + @property + def ValidationError(self): + """Returns the ZenML ZenMLBaseException class.""" + return self.lazy_import("zenml.exceptions", "ValidationError") + + @property + def IllegalOperationError(self) -> Any: + """Returns the IllegalOperationError class.""" + return self.lazy_import("zenml.exceptions", "IllegalOperationError") + + @property + def StackComponentValidationError(self): + """Returns the ZenML StackComponentValidationError class.""" + return self.lazy_import("zenml.exceptions", "StackComponentValidationError") + + @property + def StackComponentType(self): + """Returns the ZenML StackComponentType enum.""" + return self.lazy_import("zenml.enums", "StackComponentType") + + @property + def ZenKeyError(self) -> Any: + """Returns the ZenKeyError class.""" + return self.lazy_import("zenml.exceptions", "ZenKeyError") + + def fetch_stacks(self, args): + """Fetches all ZenML stacks and components with pagination.""" + if len(args) < 2: + return {"error": "Insufficient arguments provided."} + page, max_size = args + try: + stacks_page = self.client.list_stacks( + page=page, size=max_size, hydrate=True + ) + stacks_data = self.process_stacks(stacks_page.items) + + return { + "stacks": stacks_data, + "total": stacks_page.total, + "total_pages": stacks_page.total_pages, + "current_page": page, + "items_per_page": max_size, + } + except self.ValidationError as e: + return {"error": "ValidationError", "message": str(e)} + except self.ZenMLBaseException as e: + return [{"error": f"Failed to retrieve stacks: {str(e)}"}] + + def process_stacks(self, stacks): + """Process stacks to the desired format.""" + return [ + { + "id": str(stack.id), + "name": stack.name, + "components": { + component_type: [ + { + "id": str(component.id), + "name": component.name, + "flavor": component.flavor, + "type": component.type, + } + for component in components + ] + for component_type, components in stack.components.items() + }, + } + for stack in stacks + ] + + def get_active_stack(self) -> dict: + """Fetches the active ZenML stack. + + Returns: + dict: Dictionary containing active stack data. + """ + try: + active_stack = self.client.active_stack_model + return { + "id": str(active_stack.id), + "name": active_stack.name, + } + except self.ZenMLBaseException as e: + return {"error": f"Failed to retrieve active stack: {str(e)}"} + + def set_active_stack(self, args) -> dict: + """Sets the active ZenML stack. + + Args: + args (list): List containing the stack name or id. + Returns: + dict: Dictionary containing the active stack data. + """ + stack_name_or_id = args[0] + + if not stack_name_or_id: + return {"error": "Missing stack_name_or_id"} + + try: + self.client.activate_stack(stack_name_id_or_prefix=stack_name_or_id) + active_stack = self.client.active_stack_model + return { + "message": f"Active stack set to: {active_stack.name}", + "id": str(active_stack.id), + "name": active_stack.name, + } + except KeyError as err: + return {"error": str(err)} + + def rename_stack(self, args) -> dict: + """Renames a specified ZenML stack. + + Args: + args (list): List containing the stack name or id and the new stack name. + Returns: + dict: Dictionary containing the renamed stack data. + """ + stack_name_or_id = args[0] + new_stack_name = args[1] + + if not stack_name_or_id or not new_stack_name: + return {"error": "Missing stack_name_or_id or new_stack_name"} + + try: + self.client.update_stack( + name_id_or_prefix=stack_name_or_id, + name=new_stack_name, + ) + return { + "message": f"Stack `{stack_name_or_id}` successfully renamed to `{new_stack_name}`!" + } + except (KeyError, self.IllegalOperationError) as err: + return {"error": str(err)} + + def copy_stack(self, args) -> dict: + """Copies a specified ZenML stack to a new stack. + + Args: + args (list): List containing the source stack name or id and the target stack name. + Returns: + dict: Dictionary containing the copied stack data. + """ + source_stack_name_or_id = args[0] + target_stack_name = args[1] + + if not source_stack_name_or_id or not target_stack_name: + return { + "error": "Both source stack name/id and target stack name are required" + } + + try: + stack_to_copy = self.client.get_stack( + name_id_or_prefix=source_stack_name_or_id + ) + component_mapping = { + c_type: [c.id for c in components][0] + for c_type, components in stack_to_copy.components.items() + if components + } + + self.client.create_stack( + name=target_stack_name, components=component_mapping + ) + return { + "message": ( + f"Stack `{source_stack_name_or_id}` successfully copied " + f"to `{target_stack_name}`!" + ) + } + except ( + self.ZenKeyError, + self.StackComponentValidationError, + ) as e: + return {"error": str(e)} + + def register_stack(self, args: Tuple[str, Dict[str, str]]) -> Dict[str, str]: + """Registers a new ZenML Stack. + + Args: + args (list): List containing the name and chosen components for the stack. + Returns: + Dictionary containing a message relevant to whether the action succeeded or failed + """ + [name, components] = args + + try: + self.client.create_stack(name, components) + return {"message": f"Stack {name} successfully registered"} + except self.ZenMLBaseException as e: + return {"error": str(e)} + + def update_stack(self, args: Tuple[str, str, Dict[str, List[str]]]) -> Dict[str, str]: + """Updates a specified ZenML Stack. + + Args: + args (list): List containing the id of the stack being updated, the new name, and the chosen components. + Returns: + Dictionary containing a message relevant to whether the action succeeded or failed + """ + [id, name, components] = args + + try: + old = self.client.get_stack(id) + if old.name == name: + self.client.update_stack(name_id_or_prefix=id, component_updates=components) + else: + self.client.update_stack(name_id_or_prefix=id, name=name, component_updates=components) + + return {"message": f"Stack {name} successfully updated."} + except self.ZenMLBaseException as e: + return {"error": str(e)} + + def delete_stack(self, args: Tuple[str]) -> Dict[str, str]: + """Deletes a specified ZenML stack. + + Args: + args (list): List containing the id of the stack to delete. + Returns: + Dictionary containing a message relevant to whether the action succeeded or failed + """ + [id] = args + + try: + self.client.delete_stack(id) + + return {"message": f"Stack {id} successfully deleted."} + except self.ZenMLBaseException as e: + return {"error": str(e)} + + def register_component(self, args: Tuple[str, str, str, Dict[str, str]]) -> Dict[str, str]: + """Registers a new ZenML stack component. + + Args: + args (list): List containing the component type, flavor used, name, and configuration of the desired new component. + Returns: + Dictionary containing a message relevant to whether the action succeeded or failed + """ + [component_type, flavor, name, configuration] = args + + try: + self.client.create_stack_component(name, flavor, component_type, configuration) + + return {"message": f"Stack Component {name} successfully registered"} + except self.ZenMLBaseException as e: + return {"error": str(e)} + + def update_component(self, args: Tuple[str, str, str, Dict[str, str]]) -> Dict[str, str]: + """Updates a specified ZenML stack component. + + Args: + args (list): List containing the id, component type, new name, and desired configuration of the desired component. + Returns: + Dictionary containing a message relevant to whether the action succeeded or failed + """ + [id, component_type, name, configuration] = args + + try: + old = self.client.get_stack_component(component_type, id) + + new_name = None if old.name == name else name + + self.client.update_stack_component(id, component_type, name=new_name, configuration=configuration) + + return {"message": f"Stack Component {name} successfully updated"} + except self.ZenMLBaseException as e: + return {"error": str(e)} + + def delete_component(self, args: Tuple[str, str]) -> Dict[str, str]: + """Deletes a specified ZenML stack component. + + Args: + args (list): List containing the id and component type of the desired component. + Returns: + Dictionary containing a message relevant to whether the action succeeded or failed + """ + [id, component_type] = args + + try: + self.client.delete_stack_component(id, component_type) + + return {"mesage": f"Stack Component {id} successfully deleted"} + except self.ZenMLBaseException as e: + return {"error": str(e)} + + def list_components(self, args: Tuple[int, int, Union[str, None]]) -> Union[ListComponentsResponse,ErrorResponse]: + """Lists stack components in a paginated way. + + Args: + args (list): List containing the page, maximum items per page, and an optional type filter used to retrieve expected components. + Returns: + A Dictionary containing the paginated results or an error message specifying why the action failed. + """ + if len(args) < 2: + return {"error": "Insufficient arguments provided."} + + page = args[0] + max_size = args[1] + filter = None + + if len(args) >= 3: + filter = args[2] + + try: + components = self.client.list_stack_components(page=page, size=max_size, type=filter, hydrate=True) + + return { + "index": components.index, + "max_size": components.max_size, + "total_pages": components.total_pages, + "total": components.total, + "items": [ + { + "id": str(item.id), + "name": item.name, + "flavor": item.body.flavor, + "type": item.body.type, + "config": item.metadata.configuration, + } + for item in components.items + ], + } + except self.ZenMLBaseException as e: + return {"error": f"Failed to retrieve list of stack components: {str(e)}"} + + def get_component_types(self) -> Union[List[str], ErrorResponse]: + """Gets a list of all component types. + + Returns: + A list of component types or a dictionary containing an error message specifying why the action failed. + """ + try: + return self.StackComponentType.values() + except self.ZenMLBaseException as e: + return {"error": f"Failed to retrieve list of component types: {str(e)}"} + + def list_flavors(self, args: Tuple[int, int, Optional[str]]) -> Union[ListFlavorsResponse, ErrorResponse]: + """Lists stack component flavors in a paginated way. + + Args: + args (list): List containing page, max items per page, and an optional component type filter used to retrieve expected component flavors. + Returns: + A Dictionary containing the paginated results or an error message specifying why the action failed. + """ + if len(args) < 2: + return {"error": "Insufficient arguments provided."} + + page = args[0] + max_size = args[1] + filter = None + if len(args) >= 3: + filter = args[2] + + try: + flavors = self.client.list_flavors(page=page, size=max_size, type=filter, hydrate=True) + + return { + "index": flavors.index, + "max_size": flavors.max_size, + "total_pages": flavors.total_pages, + "total": flavors.total, + "items": [ + { + "id": str(flavor.id), + "name": flavor.name, + "type": flavor.body.type, + "logo_url": flavor.body.logo_url, + "config_schema": flavor.metadata.config_schema, + "docs_url": flavor.metadata.docs_url, + "sdk_docs_url": flavor.metadata.sdk_docs_url, + "connector_type": flavor.metadata.connector_type, + "connector_resource_type": flavor.metadata.connector_resource_type, + "connector_resource_id_attr": flavor.metadata.connector_resource_id_attr, + } for flavor in flavors.items + ] + } + + except self.ZenMLBaseException as e: + return {"error": f"Failed to retrieve list of flavors: {str(e)}"} \ No newline at end of file diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..24946bed --- /dev/null +++ b/mypy.ini @@ -0,0 +1,9 @@ +[mypy] +python_version = 3.8 +ignore_missing_imports = True +check_untyped_defs = True +disallow_untyped_defs = False +warn_unused_ignores = True + +[mypy-bundled.tool.*] +ignore_errors = False diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000..ed856c66 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,179 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""All the action we need during build""" + +import json +import os +import pathlib +import urllib.request as url_lib +from typing import List + +import nox # pylint: disable=import-error + + +def _install_bundle(session: nox.Session) -> None: + session.install( + "-t", + "./bundled/libs", + "--no-cache-dir", + "--implementation", + "py", + "--no-deps", + "--upgrade", + "-r", + "./requirements.txt", + ) + + +def _check_files(names: List[str]) -> None: + root_dir = pathlib.Path(__file__).parent + for name in names: + file_path = root_dir / name + lines: List[str] = file_path.read_text().splitlines() + if any(line for line in lines if line.startswith("# TODO:")): + # pylint: disable=broad-exception-raised + raise Exception(f"Please update {os.fspath(file_path)}.") + + +def _update_pip_packages(session: nox.Session) -> None: + session.run( + "pip-compile", + "--generate-hashes", + "--resolver=backtracking", + "--upgrade", + "./requirements.in", + ) + session.run( + "pip-compile", + "--generate-hashes", + "--resolver=backtracking", + "--upgrade", + "./src/test/python_tests/requirements.in", + ) + + +def _get_package_data(package): + json_uri = f"https://registry.npmjs.org/{package}" + with url_lib.urlopen(json_uri) as response: + return json.loads(response.read()) + + +def _update_npm_packages(session: nox.Session) -> None: + pinned = { + "vscode-languageclient", + "@types/vscode", + "@types/node", + } + package_json_path = pathlib.Path(__file__).parent / "package.json" + package_json = json.loads(package_json_path.read_text(encoding="utf-8")) + + for package in package_json["dependencies"]: + if package not in pinned: + data = _get_package_data(package) + latest = "^" + data["dist-tags"]["latest"] + package_json["dependencies"][package] = latest + + for package in package_json["devDependencies"]: + if package not in pinned: + data = _get_package_data(package) + latest = "^" + data["dist-tags"]["latest"] + package_json["devDependencies"][package] = latest + + # Ensure engine matches the package + if ( + package_json["engines"]["vscode"] + != package_json["devDependencies"]["@types/vscode"] + ): + print( + "Please check VS Code engine version and @types/vscode version in package.json." + ) + + new_package_json = json.dumps(package_json, indent=4) + # JSON dumps uses \n for line ending on all platforms by default + if not new_package_json.endswith("\n"): + new_package_json += "\n" + package_json_path.write_text(new_package_json, encoding="utf-8") + session.run("npm", "install", external=True) + + +def _setup_template_environment(session: nox.Session) -> None: + session.install("wheel", "pip-tools") + session.run( + "pip-compile", + "--generate-hashes", + "--resolver=backtracking", + "--upgrade", + "./requirements.in", + ) + session.run( + "pip-compile", + "--generate-hashes", + "--resolver=backtracking", + "--upgrade", + "./src/test/python_tests/requirements.in", + ) + _install_bundle(session) + + +@nox.session() +def setup(session: nox.Session) -> None: + """Sets up the template for development.""" + _setup_template_environment(session) + print(f"DEBUG – Virtual Environment Interpreter: {session.bin}/python") + + +@nox.session() +def tests(session: nox.Session) -> None: + """Runs all the tests for the extension.""" + session.install("-r", "src/test/python_tests/requirements.txt") + session.run("pytest", "src/test/python_tests") + + +@nox.session() +def lint(session: nox.Session) -> None: + """Runs linter and formatter checks on python files.""" + session.install("-r", "./requirements.txt") + session.install("-r", "src/test/python_tests/requirements.txt") + + session.install("pylint") + session.run("pylint", "-d", "W0511", "./bundled/tool") + session.run( + "pylint", + "-d", + "W0511", + "--ignore=./src/test/python_tests/test_data", + "./src/test/python_tests", + ) + session.run("pylint", "-d", "W0511", "noxfile.py") + + # check formatting using black + session.install("black") + session.run("black", "--check", "./bundled/tool") + session.run("black", "--check", "./src/test/python_tests") + session.run("black", "--check", "noxfile.py") + + # check import sorting using isort + session.install("isort") + session.run("isort", "--check", "./bundled/tool") + session.run("isort", "--check", "./src/test/python_tests") + session.run("isort", "--check", "noxfile.py") + + # check typescript code + session.run("npm", "run", "lint", external=True) + + +@nox.session() +def build_package(session: nox.Session) -> None: + """Builds VSIX package for publishing.""" + _check_files(["README.md", "LICENSE", "SECURITY.md", "SUPPORT.md"]) + _setup_template_environment(session) + session.run("npm", "install", external=True) + session.run("npm", "run", "vsce-package", external=True) + + +@nox.session() +def update_packages(session: nox.Session) -> None: + """Update pip and npm packages.""" + session.install("wheel", "pip-tools") + _update_pip_packages(session) + _update_npm_packages(session) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..413db7d4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6184 @@ +{ + "name": "zenml-vscode", + "version": "0.0.11", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "zenml-vscode", + "version": "0.0.11", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@svgdotjs/svg.js": "^3.2.4", + "@vscode/python-extension": "^1.0.5", + "axios": "^1.6.7", + "dagre": "^0.8.5", + "fs-extra": "^11.2.0", + "hbs": "^4.2.0", + "svg-pan-zoom": "github:bumbu/svg-pan-zoom", + "svgdom": "^0.1.19", + "vscode-languageclient": "^9.0.1" + }, + "devDependencies": { + "@types/dagre": "^0.7.52", + "@types/fs-extra": "^11.0.4", + "@types/hbs": "^4.0.4", + "@types/mocha": "^10.0.6", + "@types/node": "^18.19.18", + "@types/sinon": "^17.0.3", + "@types/svgdom": "^0.1.2", + "@types/vscode": "^1.86.0", + "@types/webpack": "^5.28.5", + "@typescript-eslint/eslint-plugin": "^6.19.1", + "@typescript-eslint/parser": "^6.19.1", + "@vscode/test-cli": "^0.0.4", + "@vscode/test-electron": "^2.3.9", + "@vscode/vsce": "^2.24.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "prettier": "^3.2.5", + "sinon": "^17.0.1", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "typescript": "^5.3.3", + "webpack": "^5.90.0", + "webpack-cli": "^5.1.4" + }, + "engines": { + "vscode": "^1.86.0" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "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": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "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": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "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==", + "dev": true, + "dependencies": { + "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": ">=12" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "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/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "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==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "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==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "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, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, + "node_modules/@svgdotjs/svg.js": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz", + "integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Fuzzyma" + } + }, + "node_modules/@swc/helpers": { + "version": "0.4.36", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.36.tgz", + "integrity": "sha512-5lxnyLEYFskErRPenYItLRSge5DjrJngYKdVjRSrWfza9G6KkgHEXi0vUZiyUeMU5JfXH1YnvXZzSp8ul88o2Q==", + "dependencies": { + "legacy-swc-helpers": "npm:@swc/helpers@=0.4.14", + "tslib": "^2.4.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==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/dagre": { + "version": "0.7.52", + "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.52.tgz", + "integrity": "sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw==", + "dev": true + }, + "node_modules/@types/eslint": { + "version": "8.56.7", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.7.tgz", + "integrity": "sha512-SjDvI/x3zsZnOkYZ3lCt9lOZWZLB2jIlNKz+LBgCtDurK0JZcwucxYHn1w2BJkD34dgX9Tjnak0txtq4WTggEA==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/hbs": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/hbs/-/hbs-4.0.4.tgz", + "integrity": "sha512-GH3SIb2tzDBnTByUSOIVcD6AcLufnydBllTuFAIAGMhqPNbz8GL4tLryVdNqhq0NQEb5mVpu2FJOrUeqwJrPtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "handlebars": "^4.1.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mocha": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", + "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.19.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.29.tgz", + "integrity": "sha512-5pAX7ggTmWZdhUrhRWLPf+5oM7F80bcKVCBbr0zwEkTNzTJL2CWQjznpFgHYy6GrzkYi2Yjy7DHKoynFxqPV8g==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "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 + }, + "node_modules/@types/svgdom": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/svgdom/-/svgdom-0.1.2.tgz", + "integrity": "sha512-ZFwX8cDhbz6jiv3JZdMVYq8SSWHOUchChPmRoMwdIu3lz89aCu/gVK9TdR1eeb0ARQ8+5rtjUKrk1UR8hh0dhQ==", + "dev": true + }, + "node_modules/@types/vscode": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.87.0.tgz", + "integrity": "sha512-y3yYJV2esWr8LNjp3VNbSMWG7Y43jC8pCldG8YwiHGAQbsymkkMMt0aDT1xZIOFM2eFcNiUc+dJMx1+Z0UT8fg==", + "dev": true + }, + "node_modules/@types/webpack": { + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", + "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "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/@vscode/python-extension": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@vscode/python-extension/-/python-extension-1.0.5.tgz", + "integrity": "sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==", + "engines": { + "node": ">=16.17.1", + "vscode": "^1.78.0" + } + }, + "node_modules/@vscode/test-cli": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.4.tgz", + "integrity": "sha512-Tx0tfbxeSb2Xlo+jpd+GJrNLgKQHobhRHrYvOipZRZQYWZ82sKiK02VY09UjU1Czc/YnZnqyAnjUfaVGl3h09w==", + "dev": true, + "dependencies": { + "@types/mocha": "^10.0.2", + "chokidar": "^3.5.3", + "glob": "^10.3.10", + "minimatch": "^9.0.3", + "mocha": "^10.2.0", + "supports-color": "^9.4.0", + "yargs": "^17.7.2" + }, + "bin": { + "vscode-test": "out/bin.mjs" + } + }, + "node_modules/@vscode/test-electron": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.9.tgz", + "integrity": "sha512-z3eiChaCQXMqBnk2aHHSEkobmC2VRalFQN0ApOAtydL172zXGxTwGrRtviT5HnUB+Q+G3vtEYFtuQkYqBzYgMA==", + "dev": true, + "dependencies": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "jszip": "^3.10.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vscode/vsce": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.24.0.tgz", + "integrity": "sha512-p6CIXpH5HXDqmUkgFXvIKTjZpZxy/uDx4d/UsfhS9vQUun43KDNUbYeZocyAHgqcJlPEurgArHz9te1PPiqPyA==", + "dev": true, + "dependencies": { + "azure-devops-node-api": "^11.0.1", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "commander": "^6.2.1", + "glob": "^7.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.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": "^7.5.2", + "tmp": "^0.2.1", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 14" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@vscode/vsce/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "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/vsce/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/@webassemblyjs/ast": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "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, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "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, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.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==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.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==", + "dev": true, + "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/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "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": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/azure-devops-node-api": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz", + "integrity": "sha512-XdiGPhrpaT5J8wdERRKs5g8E0Zy1pvOYTli7z9E8nmOn3YGp4FhtjhrOyFmX/8veWCwdI69mCHKJw6l+4J/bHA==", + "dev": true, + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "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/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/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": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-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 + }, + "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.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001605", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001605.tgz", + "integrity": "sha512-nXwGlFWo34uliI9z3n6Qc0wZaf7zaZWA1CPZ169La5mV3I/gem7bst0vr5XQH5TJXZIMfDeZyOrZnSlVzKxxHQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "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, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/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, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dev": true, + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "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": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "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, + "optional": true + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "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": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/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/cliui/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/cliui/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==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "dependencies": { + "color-name": "1.1.3" + } + }, + "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 + }, + "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 + }, + "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==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "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/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "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/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, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/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-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/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/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "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/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" + } + ] + }, + "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, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "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/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.725", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.725.tgz", + "integrity": "sha512-OGkMXLY7XH6ykHE5ZOVVIMHaGAvvxqw98cswTKB683dntBJre7ufm9wouJ0ExDm0VXhHenU8mREvxIbV5nNoVQ==", + "dev": true + }, + "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==", + "dev": true + }, + "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, + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", + "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz", + "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "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-module-lexer": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", + "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==", + "dev": true + }, + "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": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "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": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "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-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/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/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/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/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/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/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/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/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "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/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/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "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" + }, + "engines": { + "node": ">=8.6.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": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "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==", + "dev": true, + "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.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fontkit": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.2.tgz", + "integrity": "sha512-jc4k5Yr8iov8QfS6u8w2CnHWVmbOGtdBtOXMze5Y+QD966Rx6PEVWXSEGwXlsDlKtu1G12cJjcsybnqhSk/+LA==", + "dependencies": { + "@swc/helpers": "^0.4.2", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==", + "license": "Apache2" + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "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, + "optional": true + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "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/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-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "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": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "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/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "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/handlebars/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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "engines": { + "node": ">=4" + } + }, + "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-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/hbs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hbs/-/hbs-4.2.0.tgz", + "integrity": "sha512-dQwHnrfWlTk5PvG9+a45GYpg0VpX47ryKF8dULVd6DtwOE6TEcYQXQ5QM6nyOx/h7v3bvEQbdn19EDAcfUAgZg==", + "license": "MIT", + "dependencies": { + "handlebars": "4.7.7", + "walk": "2.3.15" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "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/image-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz", + "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "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/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "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": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "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/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "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-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.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==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "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-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "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/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/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/jest-worker/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/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/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "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": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "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/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, + "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/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/legacy-swc-helpers": { + "name": "@swc/helpers", + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz", + "integrity": "sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "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==" + }, + "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 + }, + "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==", + "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-symbols/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/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": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/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/log-symbols/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/log-symbols/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/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" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "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==", + "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" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "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==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "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/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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": "10.4.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.4.0.tgz", + "integrity": "sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "8.1.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "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_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/mocha/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/mocha/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/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/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/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "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/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==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/mocha/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "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/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "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" + }, + "engines": { + "node": ">=10" + } + }, + "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/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": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "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==" + }, + "node_modules/nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/node-abi": { + "version": "3.57.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.57.0.tgz", + "integrity": "sha512-Dp+A9JWxRaKuHP35H77I4kCKesDy5HUDEmScia2FyncMTOXASMyg251F5PhFoDA5uqBrDDffiLpbqnrZmNXW+g==", + "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-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "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/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "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/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-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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/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-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/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dev": true, + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "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/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "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==", + "dev": true + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "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/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": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "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, + "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/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/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": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "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==", + "dev": true, + "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/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/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==" + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "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/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "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/rimraf/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/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, + "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.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/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "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/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/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/sinon/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/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "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, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.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==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "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/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/string-width/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==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/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, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-pan-zoom": { + "version": "3.6.2", + "resolved": "git+ssh://git@github.com/bumbu/svg-pan-zoom.git#aaa68d186abab5d782191b66d2582592fe5d3c13", + "license": "BSD-2-Clause" + }, + "node_modules/svgdom": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/svgdom/-/svgdom-0.1.19.tgz", + "integrity": "sha512-gBvlZ74RECaG9VyPrj9OdakOarEKKvaXh5NVkbx9oWfAo4XnQehk75b14iOW2UjFHyZThczZ1NrPV9rDrecOVg==", + "dependencies": { + "fontkit": "^2.0.2", + "image-size": "^1.0.2", + "sax": "^1.2.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Fuzzyma" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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==", + "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/terser": { + "version": "5.30.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.3.tgz", + "integrity": "sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/terser-webpack-plugin/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/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "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==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/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": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/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/ts-loader/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/ts-loader/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/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "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-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.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, + "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.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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/uglify-js": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.0.tgz", + "integrity": "sha512-wNKHUY2hYYkf6oSFfhwwiHo4WCHzHmzcXsqXYTN9ja3iApYIFbb2U6ics9hBcYLHcYGQoAlwnZlTrf3oF+BL/Q==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "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-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "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": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "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/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/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/walk": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.15.tgz", + "integrity": "sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "foreachasync": "^3.0.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.91.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", + "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.16.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "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==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "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==", + "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/wrap-ansi-cjs/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/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==", + "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.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/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, + "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": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "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": { + "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==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "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==" + }, + "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": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "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/yargs/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/yargs/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==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/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, + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/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/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..2c9051e5 --- /dev/null +++ b/package.json @@ -0,0 +1,511 @@ +{ + "name": "zenml-vscode", + "publisher": "ZenML", + "displayName": "ZenML Studio", + "description": "Integrates ZenML directly into VS Code, enhancing machine learning workflow with support for pipelines, stacks, server management and DAG visualization.", + "version": "0.0.11", + "icon": "resources/extension-logo.png", + "preview": false, + "license": "Apache-2.0", + "categories": [ + "Machine Learning", + "Visualization" + ], + "repository": { + "type": "git", + "url": "https://github.com/zenml-io/vscode-zenml" + }, + "keywords": [ + "zenml", + "ml", + "machine learning", + "mlops", + "stack management", + "pipeline management", + "development tools" + ], + "engines": { + "vscode": "^1.86.0" + }, + "activationEvents": [ + "onStartupFinished", + "onLanguage:python", + "workspaceContains:*.py" + ], + "extensionDependencies": [ + "ms-python.python" + ], + "capabilities": { + "virtualWorkspaces": { + "supported": false, + "description": "Virtual Workspaces are not supported with ." + } + }, + "main": "./dist/extension.js", + "serverInfo": { + "name": "ZenML", + "module": "zenml-python" + }, + "scripts": { + "compile": "webpack", + "compile-tests": "tsc -p . --outDir out", + "deploy": "vsce publish", + "format": "prettier --ignore-path .gitignore --write \"**/*.+(ts|json)\"", + "format-check": "prettier --ignore-path .gitignore --check \"**/*.+(ts|json)\"", + "install": "pip install -r requirements.txt --target bundled/libs", + "lint": "eslint src --ext ts", + "package": "webpack --mode production --devtool source-map --config ./webpack.config.js", + "pretest": "npm run compile-tests && npm run compile && npm run lint", + "test": "vscode-test", + "vsce-package": "vsce package -o zenml.vsix", + "vscode:prepublish": "npm run package", + "watch": "webpack --watch", + "watch-tests": "tsc -p . -w --outDir out" + }, + "contributes": { + "configuration": { + "title": "ZenML", + "properties": { + "zenml-python.args": { + "default": [], + "description": "Arguments passed in. Each argument is a separate item in the array.", + "items": { + "type": "string" + }, + "scope": "resource", + "type": "array" + }, + "zenml-python.path": { + "default": [], + "scope": "resource", + "items": { + "type": "string" + }, + "type": "array" + }, + "zenml-python.importStrategy": { + "default": "useBundled", + "enum": [ + "useBundled", + "fromEnvironment" + ], + "enumDescriptions": [ + "Always use the bundled version of ``.", + "Use `` from environment, fallback to bundled version only if `` not available in the environment." + ], + "scope": "window", + "type": "string" + }, + "zenml-python.interpreter": { + "default": [], + "description": "When set to a path to python executable, extension will use that to launch the server and any subprocess.", + "scope": "resource", + "items": { + "type": "string" + }, + "type": "array" + }, + "zenml-python.showNotifications": { + "default": "off", + "description": "Controls when notifications are shown by this extension.", + "enum": [ + "off", + "onError", + "onWarning", + "always" + ], + "enumDescriptions": [ + "All notifications are turned off, any errors or warning are still available in the logs.", + "Notifications are shown only in the case of an error.", + "Notifications are shown for errors and warnings.", + "Notifications are show for anything that the server chooses to show." + ], + "scope": "machine", + "type": "string" + }, + "zenml.serverUrl": { + "type": "string", + "default": "", + "description": "ZenML Server URL" + }, + "zenml.accessToken": { + "type": "string", + "default": "", + "description": "Access token for the ZenML server" + }, + "zenml.activeStackId": { + "type": "string", + "default": "", + "description": "Active stack id for the ZenML server" + } + } + }, + "commands": [ + { + "command": "zenml.promptForInterpreter", + "title": "Select Python Interpreter", + "category": "ZenML" + }, + { + "command": "zenml-python.restart", + "title": "Restart LSP Server", + "category": "ZenML" + }, + { + "command": "zenml.connectServer", + "title": "Connect", + "category": "ZenML Server" + }, + { + "command": "zenml.disconnectServer", + "title": "Disconnect", + "category": "ZenML Server" + }, + { + "command": "zenml.refreshServerStatus", + "title": "Refresh Server Status", + "icon": "$(refresh)", + "category": "ZenML Server" + }, + { + "command": "zenml.setStackItemsPerPage", + "title": "Set Stacks Per Page", + "icon": "$(layers)", + "category": "ZenML Stacks" + }, + { + "command": "zenml.refreshStackView", + "title": "Refresh Stack View", + "icon": "$(refresh)", + "category": "ZenML Stacks" + }, + { + "command": "zenml.getActiveStack", + "title": "Get Active Stack", + "category": "ZenML Stacks" + }, + { + "command": "zenml.registerStack", + "title": "Register New Stack", + "icon": "$(add)", + "category": "ZenML Stacks" + }, + { + "command": "zenml.updateStack", + "title": "Update Stack", + "icon": "$(edit)", + "category": "ZenML Stacks" + }, + { + "command": "zenml.setActiveStack", + "title": "Set Active Stack", + "icon": "$(check)", + "category": "ZenML Stacks" + }, + { + "command": "zenml.copyStack", + "title": "Copy Stack", + "icon": "$(copy)", + "category": "ZenML Stacks" + }, + { + "command": "zenml.goToStackUrl", + "title": "Go to URL", + "icon": "$(globe)", + "category": "ZenML Stacks" + }, + { + "command": "zenml.deleteStack", + "title": "Delete Stack", + "icon": "$(trash)", + "category": "ZenML Stacks" + }, + { + "command": "zenml.setComponentItemsPerPage", + "title": "Set Components Per Page", + "icon": "$(layers)", + "category": "ZenML Components" + }, + { + "command": "zenml.refreshComponentView", + "title": "Refresh Component View", + "icon": "$(refresh)", + "category": "ZenML Components" + }, + { + "command": "zenml.registerComponent", + "title": "Register New Component", + "icon": "$(add)", + "category": "ZenML Components" + }, + { + "command": "zenml.updateComponent", + "title": "Update Component", + "icon": "$(edit)", + "category": "ZenML Components" + }, + { + "command": "zenml.deleteComponent", + "title": "Delete Component", + "icon": "$(trash)", + "category": "ZenML Components" + }, + { + "command": "zenml.setPipelineRunsPerPage", + "title": "Set Runs Per Page", + "icon": "$(layers)", + "category": "ZenML Pipeline Runs" + }, + { + "command": "zenml.refreshPipelineView", + "title": "Refresh Pipeline View", + "icon": "$(refresh)", + "category": "ZenML Pipeline Runs" + }, + { + "command": "zenml.deletePipelineRun", + "title": "Delete Pipeline Run", + "icon": "$(trash)", + "category": "ZenML Pipeline Runs" + }, + { + "command": "zenml.goToPipelineUrl", + "title": "Go to URL", + "icon": "$(globe)", + "category": "ZenML Pipeline Runs" + }, + { + "command": "zenml.renderDag", + "title": "Render DAG", + "icon": "$(type-hierarchy)", + "category": "ZenML Pipeline Runs" + }, + { + "command": "zenml.refreshEnvironmentView", + "title": "Refresh Environment View", + "icon": "$(refresh)", + "category": "ZenML Environment" + }, + { + "command": "zenml.setPythonInterpreter", + "title": "Switch Python Interpreter", + "icon": "$(arrow-swap)", + "category": "ZenML Environment" + }, + { + "command": "zenml.restartLspServer", + "title": "Restart LSP Server", + "icon": "$(debug-restart)", + "category": "ZenML Environment" + } + ], + "viewsContainers": { + "activitybar": [ + { + "id": "zenml", + "title": "ZenML", + "icon": "resources/logo.png" + } + ], + "panel": [ + { + "id": "zenmlPanel", + "title": "ZenML", + "icon": "resources/logo.png" + } + ] + }, + "views": { + "zenml": [ + { + "id": "zenmlServerView", + "name": "Server", + "icon": "$(vm)" + }, + { + "id": "zenmlStackView", + "name": "Stacks", + "icon": "$(layers)" + }, + { + "id": "zenmlComponentView", + "name": "Stack Components", + "icon": "$(extensions)" + }, + { + "id": "zenmlPipelineView", + "name": "Pipeline Runs", + "icon": "$(beaker)" + }, + { + "id": "zenmlEnvironmentView", + "name": "Environment", + "icon": "$(server-environment)" + } + ], + "zenmlPanel": [ + { + "id": "zenmlPanelView", + "name": "ZenML" + } + ] + }, + "menus": { + "view/title": [ + { + "when": "serverCommandsRegistered && view == zenmlServerView", + "command": "zenml.connectServer", + "group": "navigation" + }, + { + "when": "serverCommandsRegistered && view == zenmlServerView", + "command": "zenml.disconnectServer", + "group": "navigation" + }, + { + "when": "serverCommandsRegistered && view == zenmlServerView", + "command": "zenml.refreshServerStatus", + "group": "navigation" + }, + { + "when": "stackCommandsRegistered && view == zenmlStackView", + "command": "zenml.registerStack", + "group": "navigation@1" + }, + { + "when": "stackCommandsRegistered && view == zenmlStackView", + "command": "zenml.setStackItemsPerPage", + "group": "navigation@2" + }, + { + "when": "stackCommandsRegistered && view == zenmlStackView", + "command": "zenml.refreshStackView", + "group": "navigation@3" + }, + { + "when": "componentCommandsRegistered && view == zenmlComponentView", + "command": "zenml.registerComponent", + "group": "navigation@1" + }, + { + "when": "componentCommandsRegistered && view == zenmlComponentView", + "command": "zenml.setComponentItemsPerPage", + "group": "navigation@2" + }, + { + "when": "componentCommandsRegistered && view == zenmlComponentView", + "command": "zenml.refreshComponentView", + "group": "navigation@3" + }, + { + "when": "pipelineCommandsRegistered && view == zenmlPipelineView", + "command": "zenml.setPipelineRunsPerPage", + "group": "navigation@1" + }, + { + "when": "pipelineCommandsRegistered && view == zenmlPipelineView", + "command": "zenml.refreshPipelineView", + "group": "navigation@2" + }, + { + "when": "environmentCommandsRegistered && view == zenmlEnvironmentView", + "command": "zenml.restartLspServer", + "group": "navigation" + } + ], + "view/item/context": [ + { + "when": "stackCommandsRegistered && view == zenmlStackView && viewItem == stack", + "command": "zenml.setActiveStack", + "group": "inline@1" + }, + { + "when": "stackCommandsRegistered && view == zenmlStackView && viewItem == stack", + "command": "zenml.updateStack", + "group": "inline@2" + }, + { + "when": "stackCommandsRegistered && view == zenmlStackView && viewItem == stack", + "command": "zenml.copyStack", + "group": "inline@3" + }, + { + "when": "stackCommandsRegistered && view == zenmlStackView && viewItem == stack", + "command": "zenml.goToStackUrl", + "group": "inline@4" + }, + { + "when": "stackCommandsRegistered && view == zenmlStackView && viewItem == stack", + "command": "zenml.deleteStack", + "group": "inline@5" + }, + { + "when": "componentCommandsRegistered && view == zenmlComponentView && viewItem == stackComponent", + "command": "zenml.updateComponent", + "group": "inline@1" + }, + { + "when": "componentCommandsRegistered && view == zenmlComponentView && viewItem == stackComponent", + "command": "zenml.deleteComponent", + "group": "inline@2" + }, + { + "when": "pipelineCommandsRegistered && view == zenmlPipelineView && viewItem == pipelineRun", + "command": "zenml.deletePipelineRun", + "group": "inline@2" + }, + { + "when": "pipelineCommandsRegistered && view == zenmlPipelineView && viewItem == pipelineRun", + "command": "zenml.goToPipelineUrl", + "group": "inline@3" + }, + { + "when": "pipelineCommandsRegistered && view == zenmlPipelineView && viewItem == pipelineRun", + "command": "zenml.renderDag", + "group": "inline@1" + }, + { + "when": "environmentCommandsRegistered && view == zenmlEnvironmentView && viewItem == interpreter", + "command": "zenml.setPythonInterpreter", + "group": "inline" + } + ] + } + }, + "devDependencies": { + "@types/dagre": "^0.7.52", + "@types/fs-extra": "^11.0.4", + "@types/hbs": "^4.0.4", + "@types/mocha": "^10.0.6", + "@types/node": "^18.19.18", + "@types/sinon": "^17.0.3", + "@types/svgdom": "^0.1.2", + "@types/vscode": "^1.86.0", + "@types/webpack": "^5.28.5", + "@typescript-eslint/eslint-plugin": "^6.19.1", + "@typescript-eslint/parser": "^6.19.1", + "@vscode/test-cli": "^0.0.4", + "@vscode/test-electron": "^2.3.9", + "@vscode/vsce": "^2.24.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "prettier": "^3.2.5", + "sinon": "^17.0.1", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "typescript": "^5.3.3", + "webpack": "^5.90.0", + "webpack-cli": "^5.1.4" + }, + "dependencies": { + "@svgdotjs/svg.js": "^3.2.4", + "@vscode/python-extension": "^1.0.5", + "axios": "^1.6.7", + "dagre": "^0.8.5", + "fs-extra": "^11.2.0", + "hbs": "^4.2.0", + "svg-pan-zoom": "github:bumbu/svg-pan-zoom", + "svgdom": "^0.1.19", + "vscode-languageclient": "^9.0.1" + } +} diff --git a/package.nls.json b/package.nls.json new file mode 100644 index 00000000..426b804f --- /dev/null +++ b/package.nls.json @@ -0,0 +1,37 @@ +{ + "extension.description": "Integrates ZenML directly into VS Code, enhancing machine learning workflow with support for pipelines, stacks, and server management.", + "command.promptForInterpreter": "Prompt to select a Python interpreter from the command palette in VSCode.", + "command.connectServer": "Establishes a connection to a specified ZenML server.", + "command.disconnectServer": "Disconnects from the currently connected ZenML server.", + "command.refreshServerStatus": "Refreshes the status of the ZenML server to reflect the current state.", + "command.refreshStackView": "Refreshes the stack view to display the latest information about ZenML stacks.", + "command.setStackItemsPerPage": "Sets the number of stacks to display per page in the stack view.", + "command.nextStackPage": "Navigates to the next page of stacks in the stack view.", + "command.previousStackPage": "Navigates to the previous page of stacks in the stack view.", + "command.getActiveStack": "Retrieves the currently active stack in ZenML.", + "command.renameStack": "Renames a specified ZenML stack.", + "command.setActiveStack": "Sets a specified stack as the active ZenML stack.", + "command.copyStack": "Creates a copy of a specified ZenML stack.", + "command.goToStackUrl": "Opens the URL of the specific ZenML stack in the Dashboard", + "command.refreshPipelineView": "Refreshes the pipeline view to display the latest information on ZenML pipeline runs.", + "command.setPipelineRunsPerPage": "Sets the number of pipeline runs to display per page in the pipeline view.", + "command.nextPipelineRunsPage": "Navigates to the next page of pipeline runs in the pipeline view.", + "command.previousPipelineRunsPage": "Navigates to the previous page of pipeline runs in the pipeline view.", + "command.deletePipelineRun": "Deletes a specified ZenML pipeline run.", + "command.goToPipelineUrl": "Opens the URL of the specific ZenML pipeline in the Dashboard", + "command.setPythonInterpreter": "Sets the Python interpreter for ZenML-related tasks within the VS Code environment.", + "command.refreshEnvironmentView": "Updates the Environment View with the latest ZenML configuration and system information.", + "command.restartLspServer": "Restarts the Language Server to ensure the latest configurations are used.", + "settings.args.description": "Specify arguments to pass to the ZenML CLI. Provide each argument as a separate item in the array.", + "settings.path.description": "Defines the path to the ZenML CLI. If left as an empty array, the default system path will be used.", + "settings.importStrategy.description": "Determines which ZenML CLI to use. Options include using a bundled version (`useBundled`) or attempting to use the CLI installed in the current Python environment (`fromEnvironment`).", + "settings.showNotifications.description": "Controls when notifications are shown by this extension.", + "settings.showNotifications.off.description": "All notifications are turned off, any errors or warnings when formatting Python files are still available in the logs.", + "settings.showNotifications.onError.description": "Notifications are shown only in the case of an error when formatting Python files.", + "settings.showNotifications.onWarning.description": "Notifications are shown for any errors and warnings when formatting Python files.", + "settings.showNotifications.always.description": "Notifications are show for anything that the server chooses to show when formatting Python files.", + "settings.interpreter.description": "Sets the path to the Python interpreter used by ZenML. This is used for launching the server and subprocesses. Leave empty to use the default interpreter.", + "settings.serverUrl.description": "URL to connect to the ZenML server.", + "settings.accessToken.description": "Access token required for authenticating with the ZenML server (not necessary currently).", + "settings.activeStackId.description": "Identifier for the currently active stack in ZenML. This is used to specify which stack is being used for operations." +} diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..452b8912 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,8 @@ +autoflake +black +isort +mypy +ruff +yamlfix +PyYAML +types-PyYAML diff --git a/requirements.in b/requirements.in new file mode 100644 index 00000000..565aba9b --- /dev/null +++ b/requirements.in @@ -0,0 +1,20 @@ +# This file is used to generate requirements.txt. +# NOTE: +# Use Python 3.8 or greater which ever is the minimum version of the python +# you plan on supporting when creating the environment or using pip-tools. +# Only run the commands below to manually upgrade packages in requirements.txt: +# 1) python -m pip install pip-tools +# 2) pip-compile --generate-hashes --resolver=backtracking --upgrade ./requirements.in +# If you are using nox commands to setup or build package you don't need to +# run the above commands manually. + +# Required packages +pygls +packaging +# Tool-specific packages for ZenML extension +watchdog +PyYAML +types-PyYAML +# Conda env fixes: Ensure hash generation for typing-extensions and exceptiongroup on Python < 3.11 +typing-extensions>=4.1.0,!=4.6.3; python_version < "3.11" +exceptiongroup==1.2.0; python_version < '3.11' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..8bd80588 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,133 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --generate-hashes ./requirements.in +# +attrs==23.2.0 \ + --hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \ + --hash=sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1 + # via + # cattrs + # lsprotocol +cattrs==23.2.3 \ + --hash=sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108 \ + --hash=sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f + # via + # lsprotocol + # pygls +exceptiongroup==1.2.0 ; python_version < "3.11" \ + --hash=sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 \ + --hash=sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68 + # via + # -r ./requirements.in + # cattrs +lsprotocol==2023.0.1 \ + --hash=sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2 \ + --hash=sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d + # via pygls +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 + # via -r ./requirements.in +pygls==1.3.1 \ + --hash=sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018 \ + --hash=sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e + # via -r ./requirements.in +pyyaml==6.0.1 \ + --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ + --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ + --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \ + --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ + --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ + --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ + --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ + --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ + --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ + --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ + --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \ + --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \ + --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ + --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \ + --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ + --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ + --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ + --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ + --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ + --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ + --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ + --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \ + --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ + --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ + --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ + --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \ + --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ + --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ + --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ + --hash=sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef \ + --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ + --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ + --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ + --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ + --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ + --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ + --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ + --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ + --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ + --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ + --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ + --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ + --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \ + --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ + --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \ + --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ + --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ + --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ + --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ + --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ + --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f + # via -r ./requirements.in +types-pyyaml==6.0.12.20240311 \ + --hash=sha256:a9e0f0f88dc835739b0c1ca51ee90d04ca2a897a71af79de9aec5f38cb0a5342 \ + --hash=sha256:b845b06a1c7e54b8e5b4c683043de0d9caf205e7434b3edc678ff2411979b8f6 + # via -r ./requirements.in +typing-extensions==4.12.2 ; python_version < "3.11" \ + --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ + --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 + # via + # -r ./requirements.in + # cattrs +watchdog==4.0.1 \ + --hash=sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7 \ + --hash=sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767 \ + --hash=sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175 \ + --hash=sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459 \ + --hash=sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5 \ + --hash=sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429 \ + --hash=sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6 \ + --hash=sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d \ + --hash=sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7 \ + --hash=sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28 \ + --hash=sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235 \ + --hash=sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57 \ + --hash=sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a \ + --hash=sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5 \ + --hash=sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709 \ + --hash=sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee \ + --hash=sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84 \ + --hash=sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd \ + --hash=sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba \ + --hash=sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db \ + --hash=sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682 \ + --hash=sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35 \ + --hash=sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d \ + --hash=sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645 \ + --hash=sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253 \ + --hash=sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193 \ + --hash=sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b \ + --hash=sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44 \ + --hash=sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b \ + --hash=sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625 \ + --hash=sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e \ + --hash=sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5 + # via -r ./requirements.in diff --git a/resources/components-form/components.css b/resources/components-form/components.css new file mode 100644 index 00000000..d8b7766a --- /dev/null +++ b/resources/components-form/components.css @@ -0,0 +1,87 @@ +/* Copyright(c) ZenML GmbH 2024. All Rights Reserved. + Licensed under the Apache License, Version 2.0(the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + or implied.See the License for the specific language governing + permissions and limitations under the License. */ +.container { + max-width: 600px; + margin: auto; +} + +.block { + padding: 10px 10px 5px 10px; + border: 2px var(--vscode-editor-foreground) solid; + margin-bottom: 10px; +} + +.logo { + float: right; + max-width: 100px; +} + +.docs { + clear: both; + display: flex; + justify-content: space-around; + padding: 5px 0px; +} + +.button, +button { + padding: 2px 5px; + background-color: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); + border: 2px var(--vscode-editor-foreground) solid; + border-radius: 10px; +} + +.field { + margin-bottom: 10px; +} + +.value { + width: 100%; + box-sizing: border-box; + padding-left: 20px; +} + +.input { + width: 100%; +} + +.center { + display: flex; + align-items: center; + justify-content: center; +} + +.loader { + width: 20px; + height: 20px; + border: 5px solid #fff; + border-bottom-color: #ff3d00; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; +} + +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.hidden { + display: none; +} diff --git a/resources/components-form/components.js b/resources/components-form/components.js new file mode 100644 index 00000000..f759ea55 --- /dev/null +++ b/resources/components-form/components.js @@ -0,0 +1,160 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +const form = document.querySelector('form'); +const submit = document.querySelector('input[type="submit"]'); +const spinner = document.querySelector('.loader'); +const title = document.querySelector('h2'); + +let mode = 'register'; +let type = ''; +let flavor = ''; +let id = ''; + +const inputs = {}; + +document.querySelectorAll('.input').forEach(element => { + inputs[element.id] = element; + if (element instanceof HTMLTextAreaElement) { + element.addEventListener('input', evt => { + try { + const val = JSON.parse(evt.target.value); + if (evt.target.dataset.array && !Array.isArray(val)) { + element.setCustomValidity('Must be an array'); + element.reportValidity(); + return; + } + } catch { + element.setCustomValidity('Invalid JSON value'); + element.reportValidity(); + return; + } + element.setCustomValidity(''); + }); + } +}); + +const setValues = (name, config) => { + document.querySelector('[name="name"]').value = name; + + for (const key in config) { + if ( + config[key] === null || + !inputs[key] || + (inputs[key].classList.contains('hidden') && !config[key]) + ) { + continue; + } + + if (typeof config[key] === 'boolean' && config[key]) { + inputs[config].checked = 'on'; + } + + if (typeof config[key] === 'object') { + inputs[key].value = JSON.stringify(config[key]); + } else { + inputs[key].value = String(config[key]); + } + + if (inputs[key].classList.contains('hidden')) { + inputs[key].classList.toggle('hidden'); + button = document.querySelector(`[data-id="${inputs[key].id}"]`); + button.textContent = '-'; + } + } +}; + +form.addEventListener('click', evt => { + const target = evt.target; + if (!(target instanceof HTMLButtonElement)) { + return; + } + + evt.preventDefault(); + + const current = target.textContent; + target.textContent = current === '+' ? '-' : '+'; + const fieldName = target.dataset.id; + const field = document.getElementById(fieldName); + field.classList.toggle('hidden'); +}); + +(() => { + const vscode = acquireVsCodeApi(); + + form.addEventListener('submit', evt => { + evt.preventDefault(); + + const data = Object.fromEntries(new FormData(form)); + + for (const id in inputs) { + if (inputs[id].classList.contains('hidden')) { + data[id] = null; + continue; + } + + if (inputs[id] instanceof HTMLTextAreaElement) { + data[id] = JSON.parse(inputs[id].value); + } + + if (inputs[id].type === 'checkbox') { + data[id] = !!inputs[id].checked; + } + + if (inputs[id].type === 'number') { + data[id] = Number(inputs[id].value); + } + } + + data.flavor = flavor; + data.type = type; + + submit.disabled = true; + spinner.classList.remove('hidden'); + + if (mode === 'update') { + data.id = id; + } + + vscode.postMessage({ + command: mode, + data, + }); + }); +})(); + +window.addEventListener('message', evt => { + const message = evt.data; + + switch (message.command) { + case 'register': + mode = 'register'; + type = message.type; + flavor = message.flavor; + id = ''; + break; + + case 'update': + mode = 'update'; + type = message.type; + flavor = message.flavor; + id = message.id; + title.innerText = title.innerText.replace('Register', 'Update'); + setValues(message.name, message.config); + break; + + case 'fail': + spinner.classList.add('hidden'); + submit.disabled = false; + break; + } +}); diff --git a/resources/dag-view/dag.css b/resources/dag-view/dag.css new file mode 100644 index 00000000..25b6ce6d --- /dev/null +++ b/resources/dag-view/dag.css @@ -0,0 +1,157 @@ +/* Copyright(c) ZenML GmbH 2024. All Rights Reserved. + Licensed under the Apache License, Version 2.0(the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + or implied.See the License for the specific language governing + permissions and limitations under the License. */ +body { + background-color: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); +} + +body > svg { + z-index: 0; + width: 100%; +} + +#edges polyline { + stroke: var(--vscode-editor-foreground); +} + +.node { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + z-index: 1; +} + +.node > div { + display: flex; + justify-content: center; + align-items: center; + border: 2px var(--vscode-editor-foreground) solid; + width: auto; + max-width: 280px; + height: 100%; + box-sizing: border-box; + padding: 0 10px; + background: var(--vscode-editor-background); + cursor: pointer; +} + +.node > .step { + border-radius: 10px; + color: lightblue; + background: rgb(0, 10, 50); +} + +.node > .artifact { + border-radius: 9999px; +} + +body.vscode-light .highlight, +body.vscode-high-contrast .highlight { + filter: drop-shadow(5px 0px 8px rgb(150, 0, 150)); +} + +body.vscode-dark .highlight { + filter: drop-shadow(5px 0px 8px lightpink); +} + +body.vscode-light #edges .highlight, +body.vscode-high-contrast #edges .highlight { + stroke: rgb(150, 0, 150); + stroke-width: 4px; +} + +body.vscode-dark #edges .highlight { + stroke: lightpink; + stroke-width: 4px; +} + +.icon { + width: 24px; + height: 24px; + margin-right: 5px; +} + +.artifact .icon { + fill: var(--vscode-editor-foreground); +} + +.completed { + fill: hsl(137 85% 50%); +} + +.failed { + fill: hsl(3 81% 52%); +} + +.cached { + fill: hsl(220 35% 50%); +} + +.initializing { + fill: hsl(259 90% 50%); +} + +.running { + fill: hsl(33 96% 50%); +} + +rect.svg-pan-zoom-control-background { + fill: var(--vscode-editor-foreground); +} + +g.svg-pan-zoom-control { + fill: var(--vscode-editor-background); + fill-opacity: 100%; +} + +#update { + position: absolute; + box-sizing: border-box; + top: 0; + left: 0; + width: 100%; + height: 4vh; + padding: 0.5em; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} + +#update button { + border: none; + background-color: inherit; + color: lightblue; + display: inline-block; + text-decoration: underline; + font-weight: bold; + cursor: pointer; +} + +.needs-update { + background-color: rgb(0, 10, 50); +} + +#container { + box-sizing: border-box; + width: 100%; + height: 99vh; + padding: 4vh 0 0.25vh; +} + +#dag { + width: 100%; + height: 100%; +} diff --git a/resources/dag-view/dag.js b/resources/dag-view/dag.js new file mode 100644 index 00000000..08253921 --- /dev/null +++ b/resources/dag-view/dag.js @@ -0,0 +1,104 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import svgPanZoom from 'svg-pan-zoom'; + +(() => { + const dag = document.querySelector('#dag'); + const panZoom = svgPanZoom(dag); + panZoom.enableControlIcons(); + panZoom.setMaxZoom(40); + + const vscode = acquireVsCodeApi(); + + const resize = () => { + dag.setAttribute('width', String(window.innerWidth * 0.95) + 'px'); + dag.setAttribute('height', String(window.innerHeight * 0.95) + 'px'); + panZoom.resize(); + panZoom.fit(); + panZoom.center(); + }; + + resize(); + window.addEventListener('resize', resize); + + const edges = [...document.querySelectorAll('polyline')]; + + dag.addEventListener('mouseover', evt => { + let target = evt.target; + const parent = evt.target.closest('.node'); + + if (!parent || target === parent) { + return; + } + + if (target.tag !== 'div') { + target = target.closest('div'); + } + + const id = parent.dataset.id; + const edgesToHighlight = edges.filter(edge => edge.dataset.from === id); + target.classList.add('highlight'); + edgesToHighlight.forEach(edge => edge.classList.add('highlight')); + }); + + dag.addEventListener('click', evt => { + const stepId = evt.target.closest('[data-stepid]')?.dataset.stepid; + const artifactId = evt.target.closest('[data-artifactid]')?.dataset.artifactid; + + if (!stepId && !artifactId) { + return; + } + + if (!panZoom.isDblClickZoomEnabled()) { + // double click + if (stepId) { + vscode.postMessage({ command: 'stepUrl', id: stepId }); + } + + if (artifactId) { + vscode.postMessage({ command: 'artifactUrl', id: artifactId }); + } + return; + } + + panZoom.disableDblClickZoom(); + setTimeout(() => panZoom.enableDblClickZoom(), 500); + + if (stepId) { + vscode.postMessage({ command: 'step', id: stepId }); + } + + if (artifactId) { + vscode.postMessage({ command: 'artifact', id: artifactId }); + } + }); + + const nodes = [...document.querySelectorAll('.node > div')]; + + nodes.forEach(node => { + const id = node.parentElement.dataset.id; + + node.addEventListener('mouseleave', () => { + const edgesToHighlight = edges.filter(edge => edge.dataset.from === id); + node.classList.remove('highlight'); + edgesToHighlight.forEach(edge => edge.classList.remove('highlight')); + }); + }); + + function update() { + vscode.postMessage({ command: 'update' }); + } + + const button = document.querySelector('#update button'); + button?.addEventListener('click', update); +})(); diff --git a/resources/dag-view/icons/alert.svg b/resources/dag-view/icons/alert.svg new file mode 100644 index 00000000..0e7cf0d3 --- /dev/null +++ b/resources/dag-view/icons/alert.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/resources/dag-view/icons/cached.svg b/resources/dag-view/icons/cached.svg new file mode 100644 index 00000000..fd2bb47f --- /dev/null +++ b/resources/dag-view/icons/cached.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/resources/dag-view/icons/check.svg b/resources/dag-view/icons/check.svg new file mode 100644 index 00000000..8ff3aa9d --- /dev/null +++ b/resources/dag-view/icons/check.svg @@ -0,0 +1,4 @@ + + + diff --git a/resources/dag-view/icons/database.svg b/resources/dag-view/icons/database.svg new file mode 100644 index 00000000..ad5074b3 --- /dev/null +++ b/resources/dag-view/icons/database.svg @@ -0,0 +1,4 @@ + + + diff --git a/resources/dag-view/icons/dataflow.svg b/resources/dag-view/icons/dataflow.svg new file mode 100644 index 00000000..66c32906 --- /dev/null +++ b/resources/dag-view/icons/dataflow.svg @@ -0,0 +1,4 @@ + + + diff --git a/resources/dag-view/icons/initializing.svg b/resources/dag-view/icons/initializing.svg new file mode 100644 index 00000000..ec1bab83 --- /dev/null +++ b/resources/dag-view/icons/initializing.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/resources/dag-view/icons/play.svg b/resources/dag-view/icons/play.svg new file mode 100644 index 00000000..abea87ed --- /dev/null +++ b/resources/dag-view/icons/play.svg @@ -0,0 +1,4 @@ + + + diff --git a/resources/extension-logo.png b/resources/extension-logo.png new file mode 100644 index 00000000..4c53142d Binary files /dev/null and b/resources/extension-logo.png differ diff --git a/resources/logo.png b/resources/logo.png new file mode 100644 index 00000000..9a4c55c6 Binary files /dev/null and b/resources/logo.png differ diff --git a/resources/python.png b/resources/python.png new file mode 100644 index 00000000..517bd26a Binary files /dev/null and b/resources/python.png differ diff --git a/resources/stacks-form/stacks.css b/resources/stacks-form/stacks.css new file mode 100644 index 00000000..765dcd89 --- /dev/null +++ b/resources/stacks-form/stacks.css @@ -0,0 +1,115 @@ +/* Copyright(c) ZenML GmbH 2024. All Rights Reserved. + Licensed under the Apache License, Version 2.0(the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + or implied.See the License for the specific language governing + permissions and limitations under the License. */ +h2 { + text-align: center; +} + +input { + background-color: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); +} + +input[type='radio'] { + appearance: none; + width: 15px; + height: 15px; + border-radius: 50%; + background-clip: content-box; + border: 2px solid var(--vscode-editor-foreground); + background-color: var(--vscode-editor-background); +} + +input[type='radio']:checked { + background-color: var(--vscode-editor-foreground); + padding: 2px; +} + +p { + margin: 0; + padding: 0; +} + +.options { + display: flex; + flex-wrap: nowrap; + overflow-x: auto; + align-items: center; + width: 100%; + border: 2px var(--vscode-editor-foreground) solid; + border-radius: 5px; + scrollbar-color: var(--vscode-editor-foreground) var(--vscode-editor-background); +} + +.single-option { + display: flex; + flex-direction: row; + align-items: start; + justify-content: center; + padding: 5px; + margin: 10px; + flex-shrink: 0; +} + +.single-option input { + margin-right: 5px; +} + +.single-option label { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: #eee; + color: #111; + text-align: center; + padding: 5px; + border: 2px var(--vscode-editor-foreground) solid; + border-radius: 5px; +} + +.single-option img { + width: 50px; + height: 50px; + flex-shrink: 0; +} + +.center { + display: flex; + justify-content: center; + align-items: center; + margin: 10px; +} + +.loader { + width: 20px; + height: 20px; + border: 5px solid #fff; + border-bottom-color: #ff3d00; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; +} + +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.hidden { + display: none; +} diff --git a/resources/stacks-form/stacks.js b/resources/stacks-form/stacks.js new file mode 100644 index 00000000..f3be1915 --- /dev/null +++ b/resources/stacks-form/stacks.js @@ -0,0 +1,99 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +document.querySelector('input[name="orchestrator"]').toggleAttribute('required'); +document.querySelector('input[name="artifact_store"]').toggleAttribute('required'); + +const form = document.querySelector('form'); +const submit = document.querySelector('input[type="submit"]'); +const spinner = document.querySelector('.loader'); +let previousValues = {}; +let id = undefined; +let mode = 'register'; + +form.addEventListener('click', evt => { + const target = evt.target; + let input = null; + + if (target instanceof HTMLLabelElement) { + input = document.getElementById(target.htmlFor); + } else if (target instanceof HTMLInputElement && target.type === 'radio') { + input = target; + } + + if (!input) { + return; + } + + const value = input.value; + const name = input.name; + if (previousValues[name] === value) { + delete previousValues[name]; + input.checked = false; + } else { + previousValues[name] = value; + } +}); + +(() => { + const vscode = acquireVsCodeApi(); + + form.addEventListener('submit', evt => { + evt.preventDefault(); + submit.disabled = true; + spinner.classList.remove('hidden'); + const data = Object.fromEntries(new FormData(evt.target)); + + if (id) { + data.id = id; + } + + vscode.postMessage({ + command: mode, + data, + }); + }); +})(); + +const title = document.querySelector('h2'); +const nameInput = document.querySelector('input[name="name"]'); + +window.addEventListener('message', evt => { + const message = evt.data; + + switch (message.command) { + case 'register': + mode = 'register'; + title.innerText = 'Register Stack'; + id = undefined; + previousValues = {}; + form.reset(); + break; + + case 'update': + mode = 'update'; + title.innerText = 'Update Stack'; + id = message.data.id; + nameInput.value = message.data.name; + previousValues = message.data.components; + Object.entries(message.data.components).forEach(([type, id]) => { + const input = document.querySelector(`[name="${type}"][value="${id}"]`); + input.checked = true; + }); + break; + + case 'fail': + spinner.classList.add('hidden'); + submit.disabled = false; + break; + } +}); diff --git a/resources/zenml-extension-dag.gif b/resources/zenml-extension-dag.gif new file mode 100644 index 00000000..b52dfe76 Binary files /dev/null and b/resources/zenml-extension-dag.gif differ diff --git a/resources/zenml-extension.gif b/resources/zenml-extension.gif new file mode 100644 index 00000000..f3c30739 Binary files /dev/null and b/resources/zenml-extension.gif differ diff --git a/resources/zenml_logo.png b/resources/zenml_logo.png new file mode 100644 index 00000000..484b67e4 Binary files /dev/null and b/resources/zenml_logo.png differ diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 00000000..7739b5ac --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.8.18 \ No newline at end of file diff --git a/scripts/clear_and_compile.sh b/scripts/clear_and_compile.sh new file mode 100755 index 00000000..e45d6f6f --- /dev/null +++ b/scripts/clear_and_compile.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +echo "Removing bundled/tool/__pycache__..." +if [ -d "bundled/tool/__pycache__" ]; then + rm -rf bundled/tool/__pycache__ +fi + +echo "Removing dist directory..." +if [ -d "dist" ]; then + rm -rf dist +fi + +echo "Recompiling with npm..." +if ! command -v npm &> /dev/null +then + echo "npm could not be found. Please install npm to proceed." + exit 1 +fi +npm run compile + +echo "Operation completed." diff --git a/scripts/format.sh b/scripts/format.sh new file mode 100755 index 00000000..8d2dd7ef --- /dev/null +++ b/scripts/format.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -euxo pipefail + +# Source directory for Python tool +python_src="bundled/tool" + +echo "Formatting Python files in bundled/tool..." +find $python_src -name "*.py" -not -path "*/.mypy_cache/*" -print0 | xargs -0 autoflake --in-place --remove-all-unused-imports +find $python_src -name "*.py" -not -path "*/.mypy_cache/*" -print0 | xargs -0 isort -- +find $python_src -name "*.py" -not -path "*/.mypy_cache/*" -print0 | xargs -0 black --line-length 100 -- + +echo "Formatting TypeScript files..." +npx prettier --ignore-path .gitignore --write "**/*.+(ts|json)" + +echo "Formatting YAML files..." +find .github -name "*.yml" -print0 | xargs -0 yamlfix -- + +echo "Formatting complete." \ No newline at end of file diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100755 index 00000000..38ba0ec9 --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -euxo pipefail + +cd "$(dirname "$0")/.." || exit +printf "Working directory: %s\n" "$(pwd)" +# Set PYTHONPATH to include bundled/tool +PYTHONPATH="${PYTHONPATH-}:$(pwd)/bundled/tool" +export PYTHONPATH + +# Lint Python files with ruff +echo "Linting Python files..." +ruff check bundled/tool || { echo "Linting Python files failed"; exit 1; } + +# Type check Python files with mypy +echo "Type checking Python files with mypy..." +mypy bundled/tool || { echo "Type checking Python files with mypy failed"; exit 1; } + +# Lint YAML files with yamlfix +echo "Checking YAML files with yamlfix..." +yamlfix .github/workflows/*.yml --check || { echo "Linting YAML files failed"; exit 1; } + +unset PYTHONPATH diff --git a/src/commands/components/ComponentsForm.ts b/src/commands/components/ComponentsForm.ts new file mode 100644 index 00000000..3cc31c07 --- /dev/null +++ b/src/commands/components/ComponentsForm.ts @@ -0,0 +1,410 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. + +import * as vscode from 'vscode'; +import WebviewBase from '../../common/WebviewBase'; +import { handlebars } from 'hbs'; +import Panels from '../../common/panels'; +import { Flavor } from '../../types/StackTypes'; +import { LSClient } from '../../services/LSClient'; +import { traceError, traceInfo } from '../../common/log/logging'; +import { ComponentDataProvider } from '../../views/activityBar/componentView/ComponentDataProvider'; + +const ROOT_PATH = ['resources', 'components-form']; +const CSS_FILE = 'components.css'; +const JS_FILE = 'components.js'; + +interface ComponentField { + is_string?: boolean; + is_integer?: boolean; + is_boolean?: boolean; + is_string_object?: boolean; + is_json_object?: boolean; + is_array?: boolean; + is_optional?: boolean; + is_required?: boolean; + defaultValue: any; + title: string; + key: string; +} + +export default class ComponentForm extends WebviewBase { + private static instance: ComponentForm | null = null; + + private root: vscode.Uri; + private javaScript: vscode.Uri; + private css: vscode.Uri; + private template: HandlebarsTemplateDelegate; + + /** + * Retrieves a singleton instance of ComponentForm + * @returns {ComponentForm} The singleton instance + */ + public static getInstance(): ComponentForm { + if (!ComponentForm.instance) { + ComponentForm.instance = new ComponentForm(); + } + + return ComponentForm.instance; + } + + constructor() { + super(); + + if (WebviewBase.context === null) { + throw new Error('Extension Context Not Propagated'); + } + + this.root = vscode.Uri.joinPath(WebviewBase.context.extensionUri, ...ROOT_PATH); + this.javaScript = vscode.Uri.joinPath(this.root, JS_FILE); + this.css = vscode.Uri.joinPath(this.root, CSS_FILE); + + this.template = handlebars.compile(this.produceTemplate()); + } + + /** + * Opens a webview panel based on the flavor config schema to register a new + * component + * @param {Flavor} flavor Flavor of component to register + */ + public async registerForm(flavor: Flavor) { + const panel = await this.getPanel(); + const description = flavor.config_schema.description.replaceAll('\n', '
'); + panel.webview.html = this.template({ + type: flavor.type, + flavor: flavor.name, + logo: flavor.logo_url, + description, + docs_url: flavor.docs_url, + sdk_docs_url: flavor.sdk_docs_url, + cspSource: panel.webview.cspSource, + js: panel.webview.asWebviewUri(this.javaScript), + css: panel.webview.asWebviewUri(this.css), + fields: this.toFormFields(flavor.config_schema), + }); + + panel.webview.postMessage({ command: 'register', type: flavor.type, flavor: flavor.name }); + } + + /** + * Opens a webview panel based on the flavor config schema to update a + * specified component + * @param {Flavor} flavor Flavor of the selected component + * @param {string} name Name of the selected component + * @param {string} id ID of the selected component + * @param {object} config Current configuration settings of the selected + * component + */ + public async updateForm( + flavor: Flavor, + name: string, + id: string, + config: { [key: string]: any } + ) { + const panel = await this.getPanel(); + const description = flavor.config_schema.description.replaceAll('\n', '
'); + panel.webview.html = this.template({ + type: flavor.type, + flavor: flavor.name, + logo: flavor.logo_url, + description, + docs_url: flavor.docs_url, + sdk_docs_url: flavor.sdk_docs_url, + cspSource: panel.webview.cspSource, + js: panel.webview.asWebviewUri(this.javaScript), + css: panel.webview.asWebviewUri(this.css), + fields: this.toFormFields(flavor.config_schema), + }); + + panel.webview.postMessage({ + command: 'update', + type: flavor.type, + flavor: flavor.name, + name, + id, + config, + }); + } + + private async getPanel(): Promise { + const panels = Panels.getInstance(); + const existingPanel = panels.getPanel('component-form', true); + if (existingPanel) { + existingPanel.reveal(); + return existingPanel; + } + + const panel = panels.createPanel('component-form', 'Component Form', { + enableForms: true, + enableScripts: true, + retainContextWhenHidden: true, + }); + + this.attachListener(panel); + return panel; + } + + private attachListener(panel: vscode.WebviewPanel) { + panel.webview.onDidReceiveMessage( + async (message: { command: string; data: { [key: string]: string } }) => { + let success = false; + const data = message.data; + const { name, flavor, type, id } = data; + delete data.name; + delete data.type; + delete data.flavor; + delete data.id; + + switch (message.command) { + case 'register': + success = await this.registerComponent(name, type, flavor, data); + break; + case 'update': + success = await this.updateComponent(id, name, type, data); + break; + } + + if (!success) { + panel.webview.postMessage({ command: 'fail' }); + return; + } + + panel.dispose(); + ComponentDataProvider.getInstance().refresh(); + } + ); + } + + private async registerComponent( + name: string, + type: string, + flavor: string, + data: object + ): Promise { + const lsClient = LSClient.getInstance(); + try { + const resp = await lsClient.sendLsClientRequest('registerComponent', [ + type, + flavor, + name, + data, + ]); + + if ('error' in resp) { + vscode.window.showErrorMessage(`Unable to register component: "${resp.error}"`); + console.error(resp.error); + traceError(resp.error); + return false; + } + + traceInfo(resp.message); + } catch (e) { + vscode.window.showErrorMessage(`Unable to register component: "${e}"`); + console.error(e); + traceError(e); + return false; + } + + return true; + } + + private async updateComponent( + id: string, + name: string, + type: string, + data: object + ): Promise { + const lsClient = LSClient.getInstance(); + try { + const resp = await lsClient.sendLsClientRequest('updateComponent', [id, type, name, data]); + + if ('error' in resp) { + vscode.window.showErrorMessage(`Unable to update component: "${resp.error}"`); + console.error(resp.error); + traceError(resp.error); + return false; + } + + traceInfo(resp.message); + } catch (e) { + vscode.window.showErrorMessage(`Unable to update component: "${e}"`); + console.error(e); + traceError(e); + return false; + } + + return true; + } + + private toFormFields(configSchema: { [key: string]: any }) { + const properties = configSchema.properties; + const required = configSchema.required ?? []; + + const converted: Array = []; + for (const key in properties) { + const current: ComponentField = { + key, + title: properties[key].title, + defaultValue: properties[key].default, + }; + converted.push(current); + + if ('anyOf' in properties[key]) { + if (properties[key].anyOf.find((obj: { type: string }) => obj.type === 'null')) { + current.is_optional = true; + } + + if ( + properties[key].anyOf.find( + (obj: { type: string }) => obj.type === 'object' || obj.type === 'array' + ) + ) { + current.is_json_object = true; + } else if (properties[key].anyOf[0].type === 'string') { + current.is_string = true; + } else if (properties[key].anyOf[0].type === 'integer') { + current.is_integer = true; + } else if (properties[key].anyOf[0].type === 'boolean') { + current.is_boolean = true; + } + } + + if (required.includes(key)) { + current.is_required = true; + } + + if (!properties[key].type) { + continue; + } + + current.is_boolean = properties[key].type === 'boolean'; + current.is_string = properties[key].type === 'string'; + current.is_integer = properties[key].type === 'integer'; + if (properties[key].type === 'object' || properties[key].type === 'array') { + current.is_json_object = true; + current.defaultValue = JSON.stringify(properties[key].default); + } + + if (properties[key].type === 'array') { + current.is_array = true; + } + } + + return converted; + } + + private produceTemplate(): string { + return ` + + + + + + + + Stack Form + + +
+

Register {{type}} Stack Component ({{flavor}})

+ +
+
{{{description}}}
+
+ {{#if docs_url}} + Documentation + {{/if}} + + {{#if sdk_docs_url}} + SDK Documentation + {{/if}} +
+
+
+
+
+ +
+
+ +
+
+ + {{#each fields}} +
+
+ + {{#if is_optional}} + + {{/if}} +
+ +
+ {{#if is_string}} + + {{/if}} + + {{#if is_boolean}} + + {{/if}} + + {{#if is_integer}} + + {{/if}} + + {{#if is_json_object}} + + {{/if}} +
+
+ {{/each}} +
+
+
+
+ + + + `; + } +} diff --git a/src/commands/components/cmds.ts b/src/commands/components/cmds.ts new file mode 100644 index 00000000..36c28fb0 --- /dev/null +++ b/src/commands/components/cmds.ts @@ -0,0 +1,159 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as vscode from 'vscode'; + +import ZenMLStatusBar from '../../views/statusBar'; +import { LSClient } from '../../services/LSClient'; +import { showInformationMessage } from '../../utils/notifications'; +import Panels from '../../common/panels'; +import { ComponentDataProvider } from '../../views/activityBar/componentView/ComponentDataProvider'; +import { ComponentTypesResponse, Flavor, FlavorListResponse } from '../../types/StackTypes'; +import { getFlavor, getFlavorsOfType } from '../../common/api'; +import ComponentForm from './ComponentsForm'; +import { StackComponentTreeItem } from '../../views/activityBar'; +import { traceError, traceInfo } from '../../common/log/logging'; + +/** + * Refreshes the stack component view. + */ +const refreshComponentView = async () => { + try { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Refreshing Component View...', + cancellable: false, + }, + async progress => { + await ComponentDataProvider.getInstance().refresh(); + } + ); + } catch (e) { + vscode.window.showErrorMessage(`Failed to refresh component view: ${e}`); + traceError(`Failed to refresh component view: ${e}`); + console.error(`Failed to refresh component view: ${e}`); + } +}; + +/** + * Allows one to choose a component type and flavor, then opens the component + * form webview panel to a form specific to register a new a component of that + * type and flavor. + */ +const registerComponent = async () => { + const lsClient = LSClient.getInstance(); + try { + const types = await lsClient.sendLsClientRequest('getComponentTypes'); + + if ('error' in types) { + throw new Error(String(types.error)); + } + + const type = await vscode.window.showQuickPick(types, { + title: 'What type of component to register?', + }); + if (!type) { + return; + } + + const flavors = await getFlavorsOfType(type); + if ('error' in flavors) { + throw flavors.error; + } + + const flavorNames = flavors.map(flavor => flavor.name); + const selectedFlavor = await vscode.window.showQuickPick(flavorNames, { + title: `What flavor of a ${type} component to register?`, + }); + if (!selectedFlavor) { + return; + } + + const flavor = flavors.find(flavor => selectedFlavor === flavor.name); + await ComponentForm.getInstance().registerForm(flavor as Flavor); + } catch (e) { + vscode.window.showErrorMessage(`Unable to open component form: ${e}`); + traceError(e); + console.error(e); + } +}; + +const updateComponent = async (node: StackComponentTreeItem) => { + try { + const flavor = await getFlavor(node.component.flavor); + + await ComponentForm.getInstance().updateForm( + flavor, + node.component.name, + node.component.id, + node.component.config + ); + } catch (e) { + vscode.window.showErrorMessage(`Unable to open component form: ${e}`); + traceError(e); + console.error(e); + } +}; + +/** + * Deletes a specified Stack Component + * @param {StackComponentTreeItem} node The specified stack component to delete + */ +const deleteComponent = async (node: StackComponentTreeItem) => { + const lsClient = LSClient.getInstance(); + + const answer = await vscode.window.showWarningMessage( + `Are you sure you want to delete ${node.component.name}? This cannot be undone.`, + { modal: true }, + 'Delete' + ); + + if (!answer) { + return; + } + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: `Deleting stack component ${node.component.name}...`, + }, + async () => { + try { + const resp = await lsClient.sendLsClientRequest('deleteComponent', [ + node.component.id, + node.component.type, + ]); + + if ('error' in resp) { + throw resp.error; + } + + vscode.window.showInformationMessage(`${node.component.name} deleted`); + traceInfo(`${node.component.name} deleted`); + + ComponentDataProvider.getInstance().refresh(); + } catch (e) { + vscode.window.showErrorMessage(`Failed to delete component: ${e}`); + traceError(e); + console.error(e); + } + } + ); +}; + +export const componentCommands = { + refreshComponentView, + registerComponent, + updateComponent, + deleteComponent, +}; diff --git a/src/commands/components/registry.ts b/src/commands/components/registry.ts new file mode 100644 index 00000000..086f7c74 --- /dev/null +++ b/src/commands/components/registry.ts @@ -0,0 +1,69 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { componentCommands } from './cmds'; +import { registerCommand } from '../../common/vscodeapi'; +import { ZenExtension } from '../../services/ZenExtension'; +import { ExtensionContext, commands } from 'vscode'; +import { ComponentDataProvider } from '../../views/activityBar/componentView/ComponentDataProvider'; +import { StackComponentTreeItem } from '../../views/activityBar'; + +/** + * Registers stack component-related commands for the extension. + * + * @param {ExtensionContext} context - The context in which the extension operates, used for registering commands and managing their lifecycle. + */ +export const registerComponentCommands = (context: ExtensionContext) => { + const componentDataProvider = ComponentDataProvider.getInstance(); + try { + const registeredCommands = [ + registerCommand( + 'zenml.setComponentItemsPerPage', + async () => await componentDataProvider.updateItemsPerPage() + ), + registerCommand( + 'zenml.refreshComponentView', + async () => await componentCommands.refreshComponentView() + ), + registerCommand( + 'zenml.registerComponent', + async () => await componentCommands.registerComponent() + ), + registerCommand( + 'zenml.updateComponent', + async (node: StackComponentTreeItem) => await componentCommands.updateComponent(node) + ), + registerCommand( + 'zenml.deleteComponent', + async (node: StackComponentTreeItem) => await componentCommands.deleteComponent(node) + ), + registerCommand( + 'zenml.nextComponentPage', + async () => await componentDataProvider.goToNextPage() + ), + registerCommand( + 'zenml.previousComponentPage', + async () => await componentDataProvider.goToPreviousPage() + ), + ]; + + registeredCommands.forEach(cmd => { + context.subscriptions.push(cmd); + ZenExtension.commandDisposables.push(cmd); + }); + + commands.executeCommand('setContext', 'componentCommandsRegistered', true); + } catch (e) { + console.error('Error registering component commands:', e); + commands.executeCommand('setContext', 'componentCommandsRegistered', false); + } +}; diff --git a/src/commands/environment/cmds.ts b/src/commands/environment/cmds.ts new file mode 100644 index 00000000..9ac987b5 --- /dev/null +++ b/src/commands/environment/cmds.ts @@ -0,0 +1,95 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. + +import { ProgressLocation, commands, window } from 'vscode'; +import { getInterpreterFromWorkspaceSettings } from '../../common/settings'; +import { EnvironmentDataProvider } from '../../views/activityBar/environmentView/EnvironmentDataProvider'; +import { LSP_ZENML_CLIENT_INITIALIZED, PYTOOL_MODULE } from '../../utils/constants'; +import { LSClient } from '../../services/LSClient'; +import { EventBus } from '../../services/EventBus'; +import { REFRESH_ENVIRONMENT_VIEW } from '../../utils/constants'; + +/** + * Set the Python interpreter for the current workspace. + * + * @returns {Promise} Resolves after refreshing the view. + */ +const setPythonInterpreter = async (): Promise => { + await window.withProgress( + { + location: ProgressLocation.Window, + title: 'Refreshing server status...', + }, + async progress => { + progress.report({ increment: 10 }); + const currentInterpreter = await getInterpreterFromWorkspaceSettings(); + + await commands.executeCommand('python.setInterpreter'); + + const newInterpreter = await getInterpreterFromWorkspaceSettings(); + + if (newInterpreter === currentInterpreter) { + console.log('Interpreter selection unchanged or cancelled. No server restart required.'); + window.showInformationMessage('Interpreter selection unchanged. Restart not required.'); + return; + } + progress.report({ increment: 90 }); + console.log('Interpreter selection completed.'); + + window.showInformationMessage( + 'ZenML server will restart to apply the new interpreter settings.' + ); + // The onDidChangePythonInterpreter event will trigger the server restart. + progress.report({ increment: 100 }); + } + ); +}; + +const refreshEnvironmentView = async (): Promise => { + window.withProgress( + { + location: ProgressLocation.Notification, + title: 'Refreshing Environment View...', + cancellable: false, + }, + async () => { + EnvironmentDataProvider.getInstance().refresh(); + } + ); +}; + +const restartLSPServer = async (): Promise => { + await window.withProgress( + { + location: ProgressLocation.Window, + title: 'Restarting LSP Server...', + }, + async progress => { + progress.report({ increment: 10 }); + const lsClient = LSClient.getInstance(); + lsClient.isZenMLReady = false; + lsClient.localZenML = { is_installed: false, version: '' }; + const eventBus = EventBus.getInstance(); + eventBus.emit(REFRESH_ENVIRONMENT_VIEW); + eventBus.emit(LSP_ZENML_CLIENT_INITIALIZED, false); + await commands.executeCommand(`${PYTOOL_MODULE}.restart`); + progress.report({ increment: 100 }); + } + ); +}; + +export const environmentCommands = { + setPythonInterpreter, + refreshEnvironmentView, + restartLSPServer, +}; diff --git a/src/commands/environment/registry.ts b/src/commands/environment/registry.ts new file mode 100644 index 00000000..1153ab31 --- /dev/null +++ b/src/commands/environment/registry.ts @@ -0,0 +1,50 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { registerCommand } from '../../common/vscodeapi'; +import { environmentCommands } from './cmds'; +import { ZenExtension } from '../../services/ZenExtension'; +import { ExtensionContext, commands } from 'vscode'; + +/** + * Registers pipeline-related commands for the extension. + * + * @param {ExtensionContext} context - The context in which the extension operates, used for registering commands and managing their lifecycle. + */ +export const registerEnvironmentCommands = (context: ExtensionContext) => { + try { + const registeredCommands = [ + registerCommand( + 'zenml.setPythonInterpreter', + async () => await environmentCommands.setPythonInterpreter() + ), + registerCommand( + 'zenml.refreshEnvironmentView', + async () => await environmentCommands.refreshEnvironmentView() + ), + registerCommand( + 'zenml.restartLspServer', + async () => await environmentCommands.restartLSPServer() + ), + ]; + + registeredCommands.forEach(cmd => { + context.subscriptions.push(cmd); + ZenExtension.commandDisposables.push(cmd); + }); + + commands.executeCommand('setContext', 'environmentCommandsRegistered', true); + } catch (error) { + console.error('Error registering environment commands:', error); + commands.executeCommand('setContext', 'environmentCommandsRegistered', false); + } +}; diff --git a/src/commands/pipelines/DagRender.ts b/src/commands/pipelines/DagRender.ts new file mode 100644 index 00000000..87598125 --- /dev/null +++ b/src/commands/pipelines/DagRender.ts @@ -0,0 +1,378 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import fs from 'fs/promises'; +import * as vscode from 'vscode'; +import * as Dagre from 'dagre'; +import { ArrayXY, SVG, registerWindow } from '@svgdotjs/svg.js'; +import { PipelineTreeItem, ServerDataProvider } from '../../views/activityBar'; +import { PipelineRunDag, DagNode } from '../../types/PipelineTypes'; +import { LSClient } from '../../services/LSClient'; +import { ServerStatus } from '../../types/ServerInfoTypes'; +import { JsonObject } from '../../views/panel/panelView/PanelTreeItem'; +import { PanelDataProvider } from '../../views/panel/panelView/PanelDataProvider'; +import Panels from '../../common/panels'; +import WebviewBase from '../../common/WebviewBase'; + +const ROOT_PATH = ['resources', 'dag-view']; +const CSS_FILE = 'dag.css'; +const JS_FILE = 'dag-packed.js'; +const ICONS_DIRECTORY = '/resources/dag-view/icons/'; + +export default class DagRenderer extends WebviewBase { + private static instance: DagRenderer | undefined; + private createSVGWindow: Function = () => {}; + private iconSvgs: { [name: string]: string } = {}; + private root: vscode.Uri; + private javaScript: vscode.Uri; + private css: vscode.Uri; + + constructor() { + super(); + + if (WebviewBase.context === null) { + throw new Error('Extension Context Not Propagated'); + } + + this.root = vscode.Uri.joinPath(WebviewBase.context.extensionUri, ...ROOT_PATH); + this.javaScript = vscode.Uri.joinPath(this.root, JS_FILE); + this.css = vscode.Uri.joinPath(this.root, CSS_FILE); + + this.loadSvgWindowLib(); + this.loadIcons(WebviewBase.context.extensionPath + ICONS_DIRECTORY); + } + + /** + * Retrieves a singleton instance of DagRenderer + * + * @returns {DagRenderer} The singleton instance + */ + public static getInstance(): DagRenderer { + if (!DagRenderer.instance) { + DagRenderer.instance = new DagRenderer(); + } + + return DagRenderer.instance; + } + + /** + * Used to remove DagRenderer WebviewPanels when the extension is deactivated. + */ + public deactivate(): void { + DagRenderer.instance = undefined; + } + + /** + * Renders DAG Visualization for a piepline run into Webview Panel + * @param node The Pipeline run to render + * @returns + */ + public async createView(node: PipelineTreeItem) { + const p = Panels.getInstance(); + const existingPanel = p.getPanel(node.id); + if (existingPanel) { + existingPanel.reveal(); + return; + } + + const panel = p.createPanel(node.id, node.label as string, { + enableScripts: true, + localResourceRoots: [this.root], + }); + + panel.webview.onDidReceiveMessage(this.createMessageHandler(panel, node)); + + this.renderDag(panel, node); + } + + private createMessageHandler( + panel: vscode.WebviewPanel, + node: PipelineTreeItem + ): (message: { command: string; id: string }) => Promise { + const status = ServerDataProvider.getInstance().getCurrentStatus() as ServerStatus; + const dashboardUrl = status.dashboard_url; + const deploymentType = status.deployment_type; + const runUrl = deploymentType === 'other' ? '' : `${dashboardUrl}/runs/${node.id}?tab=overview`; + + return async (message: { command: string; id: string }): Promise => { + switch (message.command) { + case 'update': + this.renderDag(panel, node); + break; + + case 'step': + this.loadStepDataIntoPanel(message.id, runUrl); + break; + + case 'artifact': { + this.loadArtifactDataIntoPanel(message.id, runUrl, dashboardUrl, deploymentType); + break; + } + + case 'artifactUrl': + this.openArtifactUrl(message.id, dashboardUrl, deploymentType, runUrl); + break; + + case 'stepUrl': + this.openStepUrl(runUrl); + break; + } + }; + } + + private async loadStepDataIntoPanel(id: string, runUrl: string): Promise { + const dataPanel = PanelDataProvider.getInstance(); + dataPanel.setLoading(); + + const client = LSClient.getInstance(); + try { + const stepData = await client.sendLsClientRequest('getPipelineRunStep', [id]); + + dataPanel.setData({ runUrl, ...stepData }, 'Pipeline Run Step Data'); + vscode.commands.executeCommand('zenmlPanelView.focus'); + } catch (e) { + vscode.window.showErrorMessage(`Unable to retrieve step ${id}: ${e}`); + console.error(e); + } + } + + private async loadArtifactDataIntoPanel( + id: string, + runUrl: string, + dashboardUrl: string, + deploymentType: string + ) { + const dataPanel = PanelDataProvider.getInstance(); + dataPanel.setLoading(); + + const client = LSClient.getInstance(); + try { + const artifactData = await client.sendLsClientRequest('getPipelineRunArtifact', [ + id, + ]); + + if (deploymentType === 'cloud') { + const artifactUrl = `${dashboardUrl}/artifact-versions/${id}?tab=overview`; + dataPanel.setData({ artifactUrl, ...artifactData }, 'Artifact Version Data'); + } else { + dataPanel.setData({ runUrl, ...artifactData }, 'Artifact Version Data'); + } + + vscode.commands.executeCommand('zenmlPanelView.focus'); + } catch (e) { + vscode.window.showErrorMessage(`Unable to retrieve artifact version ${id}: ${e}`); + console.error(e); + } + } + + private openArtifactUrl( + id: string, + dashboardUrl: string, + deploymentType: string, + runUrl: string + ): void { + const uri = vscode.Uri.parse( + deploymentType === 'cloud' ? `${dashboardUrl}/artifact-versions/${id}?tab=overview` : runUrl + ); + vscode.env.openExternal(uri); + } + + private openStepUrl(runUrl: string): void { + const uri = vscode.Uri.parse(runUrl); + vscode.env.openExternal(uri); + } + + private async renderDag(panel: vscode.WebviewPanel, node: PipelineTreeItem) { + const client = LSClient.getInstance(); + + let dagData: PipelineRunDag; + try { + dagData = await client.sendLsClientRequest('getPipelineRunDag', [node.id]); + } catch (e) { + vscode.window.showErrorMessage(`Unable to receive response from Zenml server: ${e}`); + return; + } + + const cssUri = panel.webview.asWebviewUri(this.css); + const jsUri = panel.webview.asWebviewUri(this.javaScript); + const graph = this.layoutDag(dagData); + const svg = await this.drawDag(graph); + const updateButton = dagData.status === 'running' || dagData.status === 'initializing'; + const title = `${dagData.name}`; + + // And set its HTML content + panel.webview.html = this.getWebviewContent({ + svg, + cssUri, + jsUri, + updateButton, + title, + cspSource: panel.webview.cspSource, + }); + } + + private async loadSvgWindowLib() { + const { createSVGWindow } = await import('svgdom'); + this.createSVGWindow = createSVGWindow; + } + + private loadIcons(path: string): void { + const ICON_MAP = { + failed: 'alert.svg', + completed: 'check.svg', + cached: 'cached.svg', + initializing: 'initializing.svg', + running: 'play.svg', + database: 'database.svg', + dataflow: 'dataflow.svg', + }; + Object.entries(ICON_MAP).forEach(async ([name, fileName]) => { + try { + const file = await fs.readFile(path + fileName); + this.iconSvgs[name] = file.toString(); + } catch (e) { + this.iconSvgs[name] = ''; + console.error(`Unable to load icon ${name}: ${e}`); + } + }); + } + + private layoutDag(dagData: PipelineRunDag): Dagre.graphlib.Graph { + const { nodes, edges } = dagData; + const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); + graph.setGraph({ rankdir: 'TB', ranksep: 35, nodesep: 5 }); + + edges.forEach(edge => graph.setEdge(edge.source, edge.target)); + nodes.forEach(node => + graph.setNode(node.id, { width: 300, height: node.type === 'step' ? 50 : 44, ...node }) + ); + + Dagre.layout(graph); + return graph; + } + + private calculateEdges = ( + g: Dagre.graphlib.Graph + ): Array<{ from: string; points: ArrayXY[] }> => { + const edges = g.edges(); + return edges.map(edge => { + const currentLine = g.edge(edge).points.map(point => [point.x, point.y]); + const startNode = g.node(edge.v); + const endNode = g.node(edge.w); + + const rest = currentLine.slice(1, currentLine.length - 1); + const start = [startNode.x, startNode.y + startNode.height / 2]; + const end = [endNode.x, endNode.y - endNode.height / 2]; + const second = [startNode.x, rest[0][1]]; + const penultimate = [endNode.x, rest[rest.length - 1][1]]; + + return { + from: edge.v, + points: [start, second, ...rest, penultimate, end] as ArrayXY[], + }; + }); + }; + + private async drawDag(graph: Dagre.graphlib.Graph): Promise { + const window = this.createSVGWindow(); + const document = window.document; + + registerWindow(window, document); + const canvas = SVG().addTo(document.documentElement).id('dag'); + canvas.size(graph.graph().width, graph.graph().height); + const orthoEdges = this.calculateEdges(graph); + + const edgeGroup = canvas.group().attr('id', 'edges'); + + orthoEdges.forEach(edge => { + edgeGroup + .polyline(edge.points) + .fill('none') + .stroke({ width: 2, linecap: 'round', linejoin: 'round' }) + .attr('data-from', edge.from); + }); + + const nodeGroup = canvas.group().attr('id', 'nodes'); + + graph.nodes().forEach(nodeId => { + const node = graph.node(nodeId) as DagNode & ReturnType; + let iconSVG: string; + let status: string = ''; + const executionId = { attr: '', value: node.data.execution_id }; + + if (node.type === 'step') { + iconSVG = this.iconSvgs[node.data.status]; + status = node.data.status; + executionId.attr = 'data-stepid'; + } else { + executionId.attr = 'data-artifactid'; + if (node.data.artifact_type === 'ModelArtifact') { + iconSVG = this.iconSvgs.dataflow; + } else { + iconSVG = this.iconSvgs.database; + } + } + + const container = nodeGroup + .foreignObject(node.width, node.height) + .translate(node.x - node.width / 2, node.y - node.height / 2); + + const div = container.element('div').attr('class', 'node').attr('data-id', node.id); + + const box = div + .element('div') + .attr('class', node.type) + .attr(executionId.attr, executionId.value); + + const icon = SVG(iconSVG); + box.add(SVG(icon).attr('class', `icon ${status}`)); + box.element('p').words(node.data.name); + }); + return canvas.svg(); + } + + private getWebviewContent({ + svg, + cssUri, + jsUri, + updateButton, + title, + cspSource, + }: { + svg: string; + cssUri: vscode.Uri; + jsUri: vscode.Uri; + updateButton: boolean; + title: string; + cspSource: string; + }): string { + return ` + + + + + + + DAG + + +
+

${title}

${updateButton ? '' : ''} +
+
+ ${svg} +
+ + +`; + } +} diff --git a/src/commands/pipelines/cmds.ts b/src/commands/pipelines/cmds.ts new file mode 100644 index 00000000..b6d2628e --- /dev/null +++ b/src/commands/pipelines/cmds.ts @@ -0,0 +1,112 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { LSClient } from '../../services/LSClient'; +import { showErrorMessage, showInformationMessage } from '../../utils/notifications'; +import { PipelineTreeItem } from '../../views/activityBar'; +import { PipelineDataProvider } from '../../views/activityBar/pipelineView/PipelineDataProvider'; +import * as vscode from 'vscode'; +import { getPipelineRunDashboardUrl } from './utils'; +import DagRenderer from './DagRender'; + +/** + * Triggers a refresh of the pipeline view within the UI components. + * + * @returns {Promise} Resolves after refreshing the view. + */ +const refreshPipelineView = async (): Promise => { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: 'Refreshing server status...', + }, + async () => { + await PipelineDataProvider.getInstance().refresh(); + } + ); +}; + +/** + * Deletes a pipeline run. + * + * @param {PipelineTreeItem} node The pipeline run to delete. + * @returns {Promise} Resolves after deleting the pipeline run. + */ +const deletePipelineRun = async (node: PipelineTreeItem): Promise => { + const userConfirmation = await vscode.window.showWarningMessage( + 'Are you sure you want to delete this pipeline run?', + { modal: true }, + 'Yes', + 'No' + ); + + if (userConfirmation === 'Yes') { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: 'Deleting pipeline run...', + }, + async () => { + const runId = node.id; + try { + const lsClient = LSClient.getInstance(); + const result = await lsClient.sendLsClientRequest('deletePipelineRun', [runId]); + if (result && 'error' in result) { + throw new Error(result.error); + } + showInformationMessage('Pipeline run deleted successfully.'); + await refreshPipelineView(); + } catch (error: any) { + console.error(`Error deleting pipeline run: ${error}`); + showErrorMessage(`Failed to delete pipeline run: ${error.message}`); + } + } + ); + } +}; + +/** + * Opens the selected pipieline run in the ZenML Dashboard in the browser + * + * @param {PipelineTreeItem} node The pipeline run to open. + */ +const goToPipelineUrl = (node: PipelineTreeItem): void => { + const url = getPipelineRunDashboardUrl(node.id); + + if (url) { + try { + const parsedUrl = vscode.Uri.parse(url); + + vscode.env.openExternal(parsedUrl); + vscode.window.showInformationMessage(`Opening: ${url}`); + } catch (error) { + console.log(error); + vscode.window.showErrorMessage(`Failed to open pipeline run URL: ${error}`); + } + } +}; + +/** + * Opens the selected pipeline run in a DAG visualizer Webview Panel + * + * @param {PipelineTreeItem} node The pipeline run to render. + */ +const renderDag = (node: PipelineTreeItem): void => { + DagRenderer.getInstance()?.createView(node); +}; + +export const pipelineCommands = { + refreshPipelineView, + deletePipelineRun, + goToPipelineUrl, + renderDag, +}; diff --git a/src/commands/pipelines/registry.ts b/src/commands/pipelines/registry.ts new file mode 100644 index 00000000..fcf96846 --- /dev/null +++ b/src/commands/pipelines/registry.ts @@ -0,0 +1,67 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { pipelineCommands } from './cmds'; +import { registerCommand } from '../../common/vscodeapi'; +import { ZenExtension } from '../../services/ZenExtension'; +import { PipelineDataProvider, PipelineTreeItem } from '../../views/activityBar'; +import { ExtensionContext, commands } from 'vscode'; + +/** + * Registers pipeline-related commands for the extension. + * + * @param {ExtensionContext} context - The context in which the extension operates, used for registering commands and managing their lifecycle. + */ +export const registerPipelineCommands = (context: ExtensionContext) => { + const pipelineDataProvider = PipelineDataProvider.getInstance(); + + try { + const registeredCommands = [ + registerCommand( + 'zenml.refreshPipelineView', + async () => await pipelineCommands.refreshPipelineView() + ), + registerCommand( + 'zenml.deletePipelineRun', + async (node: PipelineTreeItem) => await pipelineCommands.deletePipelineRun(node) + ), + registerCommand( + 'zenml.goToPipelineUrl', + async (node: PipelineTreeItem) => await pipelineCommands.goToPipelineUrl(node) + ), + registerCommand( + 'zenml.renderDag', + async (node: PipelineTreeItem) => await pipelineCommands.renderDag(node) + ), + registerCommand('zenml.nextPipelineRunsPage', async () => + pipelineDataProvider.goToNextPage() + ), + registerCommand('zenml.previousPipelineRunsPage', async () => + pipelineDataProvider.goToPreviousPage() + ), + registerCommand( + 'zenml.setPipelineRunsPerPage', + async () => await pipelineDataProvider.updateItemsPerPage() + ), + ]; + + registeredCommands.forEach(cmd => { + context.subscriptions.push(cmd); + ZenExtension.commandDisposables.push(cmd); + }); + + commands.executeCommand('setContext', 'pipelineCommandsRegistered', true); + } catch (error) { + console.error('Error registering pipeline commands:', error); + commands.executeCommand('setContext', 'pipelineCommandsRegistered', false); + } +}; diff --git a/src/commands/pipelines/utils.ts b/src/commands/pipelines/utils.ts new file mode 100644 index 00000000..0276427c --- /dev/null +++ b/src/commands/pipelines/utils.ts @@ -0,0 +1,39 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. + +import { ServerDataProvider } from '../../views/activityBar'; +import { isServerStatus } from '../server/utils'; + +/** + * Gets the Dashboard URL for the corresponding ZenML pipeline run + * + * @param {string} id - The id of the ZenML pipeline run to be opened + * @returns {string} - The URL corresponding to the pipeline run in the ZenML Dashboard + */ +export const getPipelineRunDashboardUrl = (id: string): string => { + const status = ServerDataProvider.getInstance().getCurrentStatus(); + + if (!isServerStatus(status) || status.deployment_type === 'other') { + return ''; + } + + const currentServerUrl = status.dashboard_url; + + return `${currentServerUrl}/runs/${id}`; +}; + +const pipelineUtils = { + getPipelineRunDashboardUrl, +}; + +export default pipelineUtils; diff --git a/src/commands/server/cmds.ts b/src/commands/server/cmds.ts new file mode 100644 index 00000000..a6ecf9b7 --- /dev/null +++ b/src/commands/server/cmds.ts @@ -0,0 +1,124 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as vscode from 'vscode'; +import { LSClient } from '../../services/LSClient'; +import { + ConnectServerResponse, + GenericLSClientResponse, + RestServerConnectionResponse, +} from '../../types/LSClientResponseTypes'; +import { updateServerUrlAndToken } from '../../utils/global'; +import { refreshUtils } from '../../utils/refresh'; +import { ServerDataProvider } from '../../views/activityBar'; +import { promptAndStoreServerUrl } from './utils'; + +/** + * Initiates a connection to the ZenML server using a Flask service for OAuth2 authentication. + * The service handles user authentication, device authorization, and updates the global configuration upon success. + * + * @returns {Promise} Resolves after attempting to connect to the server. + */ +const connectServer = async (): Promise => { + const url = await promptAndStoreServerUrl(); + + if (!url) { + return false; + } + + return new Promise(resolve => { + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Connecting to ZenML server...', + cancellable: true, + }, + async progress => { + try { + const lsClient = LSClient.getInstance(); + const result = await lsClient.sendLsClientRequest('connect', [ + url, + ]); + + if (result && 'error' in result) { + throw new Error(result.error); + } + + const accessToken = (result as RestServerConnectionResponse).access_token; + await updateServerUrlAndToken(url, accessToken); + await refreshUtils.refreshUIComponents(); + resolve(true); + } catch (error) { + console.error('Failed to connect to ZenML server:', error); + vscode.window.showErrorMessage( + `Failed to connect to ZenML server: ${(error as Error).message}` + ); + resolve(false); + } + } + ); + }); +}; + +/** + * Disconnects from the ZenML server and clears related configuration and state in the application. + * + * @returns {Promise} Resolves after successfully disconnecting from the server. + */ +const disconnectServer = async (): Promise => { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Disconnecting from ZenML server...', + cancellable: true, + }, + async progress => { + try { + const lsClient = LSClient.getInstance(); + const result = await lsClient.sendLsClientRequest('disconnect'); + if (result && 'error' in result) { + throw result; + } + await refreshUtils.refreshUIComponents(); + } catch (error: any) { + console.error('Failed to disconnect from ZenML server:', error); + vscode.window.showErrorMessage( + 'Failed to disconnect from ZenML server: ' + error.message || error + ); + } + } + ); +}; + +/** + * Triggers a refresh of the server status within the UI components. + * + * @returns {Promise} Resolves after refreshing the server status. + */ +const refreshServerStatus = async (): Promise => { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: 'Refreshing server status...', + cancellable: false, + }, + async () => { + await ServerDataProvider.getInstance().refresh(); + } + ); +}; + +export const serverCommands = { + connectServer, + disconnectServer, + refreshServerStatus, +}; diff --git a/src/commands/server/registry.ts b/src/commands/server/registry.ts new file mode 100644 index 00000000..d84b1e16 --- /dev/null +++ b/src/commands/server/registry.ts @@ -0,0 +1,47 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { serverCommands } from './cmds'; +import { registerCommand } from '../../common/vscodeapi'; +import { ZenExtension } from '../../services/ZenExtension'; +import { ExtensionContext, commands } from 'vscode'; + +/** + * Registers server-related commands for the extension. + * + * @param {ExtensionContext} context - The context in which the extension operates, used for registering commands and managing their lifecycle. + */ +export const registerServerCommands = (context: ExtensionContext) => { + try { + const registeredCommands = [ + registerCommand('zenml.connectServer', async () => await serverCommands.connectServer()), + registerCommand( + 'zenml.disconnectServer', + async () => await serverCommands.disconnectServer() + ), + registerCommand( + 'zenml.refreshServerStatus', + async () => await serverCommands.refreshServerStatus() + ), + ]; + + registeredCommands.forEach(cmd => { + context.subscriptions.push(cmd); + ZenExtension.commandDisposables.push(cmd); + }); + + commands.executeCommand('setContext', 'serverCommandsRegistered', true); + } catch (error) { + console.error('Error registering server commands:', error); + commands.executeCommand('setContext', 'serverCommandsRegistered', false); + } +}; diff --git a/src/commands/server/utils.ts b/src/commands/server/utils.ts new file mode 100644 index 00000000..7e06197a --- /dev/null +++ b/src/commands/server/utils.ts @@ -0,0 +1,105 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as vscode from 'vscode'; +import { ServerStatus, ZenServerDetails } from '../../types/ServerInfoTypes'; +import { LSClient } from '../../services/LSClient'; +import { INITIAL_ZENML_SERVER_STATUS, PYTOOL_MODULE } from '../../utils/constants'; +import { ServerStatusInfoResponse } from '../../types/LSClientResponseTypes'; +import { ErrorTreeItem, createErrorItem } from '../../views/activityBar/common/ErrorTreeItem'; + +/** + * Prompts the user to enter the ZenML server URL and stores it in the global configuration. + */ +export async function promptAndStoreServerUrl(): Promise { + let serverUrl = await vscode.window.showInputBox({ + prompt: 'Enter the ZenML server URL', + placeHolder: 'https://', + }); + + serverUrl = serverUrl?.trim(); + + if (serverUrl) { + serverUrl = serverUrl.replace(/\/$/, ''); + // Validate the server URL format before storing + if (!/^https?:\/\/[^\s$.?#].[^\s]*$/.test(serverUrl)) { + vscode.window.showErrorMessage('Invalid server URL format.'); + return; + } + const config = vscode.workspace.getConfiguration('zenml'); + await config.update('serverUrl', serverUrl, vscode.ConfigurationTarget.Global); + } + + return serverUrl; +} + +/** + * Retrieves the server status from the language server or the provided server details. + * + * @returns {Promise} A promise that resolves with the server status, parsed from server details. + */ +export async function checkServerStatus(): Promise { + const lsClient = LSClient.getInstance(); + // For debugging + if (!lsClient.clientReady) { + return INITIAL_ZENML_SERVER_STATUS; + } + + try { + const result = await lsClient.sendLsClientRequest('serverInfo'); + if (!result || 'error' in result) { + if ('clientVersion' in result && 'serverVersion' in result) { + return createErrorItem(result); + } + } else if (isZenServerDetails(result)) { + return createServerStatusFromDetails(result); + } + } catch (error) { + console.error('Failed to fetch server information:', error); + } + return INITIAL_ZENML_SERVER_STATUS; +} + +function isZenServerDetails(response: any): response is ZenServerDetails { + return response && 'storeInfo' in response && 'storeConfig' in response; +} + +function createServerStatusFromDetails(details: ZenServerDetails): ServerStatus { + const { storeInfo, storeConfig } = details; + const { deployment_type, dashboard_url } = storeInfo; + + const dashboardUrl = + deployment_type === 'cloud' + ? dashboard_url + : deployment_type === 'other' + ? 'N/A' + : storeConfig.url; + + return { + ...storeInfo, + isConnected: storeConfig?.type === 'rest', + url: storeConfig?.url ?? 'unknown', + store_type: storeConfig?.type ?? 'unknown', + dashboard_url: dashboardUrl, + }; +} + +export function isServerStatus(obj: any): obj is ServerStatus { + return 'isConnected' in obj && 'url' in obj; +} + +export const serverUtils = { + promptAndStoreServerUrl, + checkServerStatus, + isZenServerDetails, + createServerStatusFromDetails, +}; diff --git a/src/commands/stack/StackForm.ts b/src/commands/stack/StackForm.ts new file mode 100644 index 00000000..65f6597f --- /dev/null +++ b/src/commands/stack/StackForm.ts @@ -0,0 +1,274 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. + +import * as vscode from 'vscode'; +import WebviewBase from '../../common/WebviewBase'; +import { handlebars } from 'hbs'; +import Panels from '../../common/panels'; +import { getAllFlavors, getAllStackComponents } from '../../common/api'; +import { Flavor, StackComponent } from '../../types/StackTypes'; +import { LSClient } from '../../services/LSClient'; +import { StackDataProvider } from '../../views/activityBar'; +import { traceError, traceInfo } from '../../common/log/logging'; + +type MixedComponent = { name: string; id: string; url: string }; + +const ROOT_PATH = ['resources', 'stacks-form']; +const CSS_FILE = 'stacks.css'; +const JS_FILE = 'stacks.js'; + +export default class StackForm extends WebviewBase { + private static instance: StackForm | null = null; + + private root: vscode.Uri; + private javaScript: vscode.Uri; + private css: vscode.Uri; + private template: HandlebarsTemplateDelegate; + + /** + * Retrieves a singleton instance of ComponentForm + * @returns {StackForm} The singleton instance + */ + public static getInstance(): StackForm { + if (!StackForm.instance) { + StackForm.instance = new StackForm(); + } + + return StackForm.instance; + } + + constructor() { + super(); + + if (WebviewBase.context === null) { + throw new Error('Extension Context Not Propagated'); + } + + this.root = vscode.Uri.joinPath(WebviewBase.context.extensionUri, ...ROOT_PATH); + this.javaScript = vscode.Uri.joinPath(this.root, JS_FILE); + this.css = vscode.Uri.joinPath(this.root, CSS_FILE); + + handlebars.registerHelper('capitalize', (str: string) => { + return str + .split('_') + .map(word => word[0].toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); + }); + + this.template = handlebars.compile(this.produceTemplate()); + } + + /** + * Opens a webview panel with a form to register a new stack + */ + public async registerForm() { + const panel = await this.display(); + panel.webview.postMessage({ command: 'register' }); + } + + /** + * Opens a webview panel with a form to update a specified stack + * @param {string} id The id of the specified stack + * @param {string} name The current name of the specified stack + * @param {object} components The component settings of the sepcified stack + */ + public async updateForm(id: string, name: string, components: { [type: string]: string }) { + const panel = await this.display(); + panel.webview.postMessage({ command: 'update', data: { id, name, components } }); + } + + private async display(): Promise { + const panels = Panels.getInstance(); + const existingPanel = panels.getPanel('stack-form'); + if (existingPanel) { + existingPanel.reveal(); + return existingPanel; + } + + const panel = panels.createPanel('stack-form', 'Stack Form', { + enableForms: true, + enableScripts: true, + retainContextWhenHidden: true, + }); + + await this.renderForm(panel); + this.attachListener(panel); + return panel; + } + + private attachListener(panel: vscode.WebviewPanel) { + panel.webview.onDidReceiveMessage( + async (message: { command: string; data: { [key: string]: string } }) => { + let success = false; + const data = message.data; + const { name, id } = data; + delete data.name; + delete data.id; + + switch (message.command) { + case 'register': + success = await this.registerStack(name, data); + break; + case 'update': { + const updateData = Object.fromEntries( + Object.entries(data).map(([type, id]) => [type, [id]]) + ); + success = await this.updateStack(id, name, updateData); + break; + } + } + + if (!success) { + panel.webview.postMessage({ command: 'fail' }); + return; + } + + panel.dispose(); + StackDataProvider.getInstance().refresh(); + } + ); + } + + private async registerStack( + name: string, + components: { [type: string]: string } + ): Promise { + const lsClient = LSClient.getInstance(); + try { + const resp = await lsClient.sendLsClientRequest('registerStack', [name, components]); + + if ('error' in resp) { + vscode.window.showErrorMessage(`Unable to register stack: "${resp.error}"`); + console.error(resp.error); + traceError(resp.error); + return false; + } + + traceInfo(resp.message); + } catch (e) { + vscode.window.showErrorMessage(`Unable to register stack: "${e}"`); + console.error(e); + traceError(e); + return false; + } + + return true; + } + + private async updateStack( + id: string, + name: string, + components: { [key: string]: string[] } + ): Promise { + const lsClient = LSClient.getInstance(); + try { + const types = await lsClient.sendLsClientRequest('getComponentTypes'); + if (!Array.isArray(types)) { + throw new Error('Could not get Component Types from LS Server'); + } + + // adding missing types to components object, in case we removed that type. + types.forEach(type => { + if (!components[type]) { + components[type] = []; + } + }); + + const resp = await lsClient.sendLsClientRequest('updateStack', [id, name, components]); + + if ('error' in resp) { + vscode.window.showErrorMessage(`Unable to update stack: "${resp.error}"`); + console.error(resp.error); + traceError(resp.error); + return false; + } + + traceInfo(resp.message); + } catch (e) { + vscode.window.showErrorMessage(`Unable to update stack: "${e}"`); + console.error(e); + traceError(e); + return false; + } + + return true; + } + + private async renderForm(panel: vscode.WebviewPanel) { + const flavors = await getAllFlavors(); + const components = await getAllStackComponents(); + const options = this.convertComponents(flavors, components); + const js = panel.webview.asWebviewUri(this.javaScript); + const css = panel.webview.asWebviewUri(this.css); + const cspSource = panel.webview.cspSource; + + panel.webview.html = this.template({ options, js, css, cspSource }); + } + + private convertComponents( + flavors: Flavor[], + components: { [type: string]: StackComponent[] } + ): { [type: string]: MixedComponent[] } { + const out: { [type: string]: MixedComponent[] } = {}; + + Object.keys(components).forEach(key => { + out[key] = components[key].map(component => { + return { + name: component.name, + id: component.id, + url: + flavors.find( + flavor => flavor.type === component.type && flavor.name === component.flavor + )?.logo_url ?? '', + }; + }); + }); + + return out; + } + + private produceTemplate(): string { + return ` + + + + + + + + Stack Form + + +

Register Stack

+
+ + {{#each options}} +

{{capitalize @key}}

+
+ {{#each this}} +
+ + +
+ {{/each}} +
+ {{/each}} +
+
+
+ + + + `; + } +} diff --git a/src/commands/stack/cmds.ts b/src/commands/stack/cmds.ts new file mode 100644 index 00000000..026fb78b --- /dev/null +++ b/src/commands/stack/cmds.ts @@ -0,0 +1,267 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as vscode from 'vscode'; +import { StackComponentTreeItem, StackDataProvider, StackTreeItem } from '../../views/activityBar'; +import ZenMLStatusBar from '../../views/statusBar'; +import { getStackDashboardUrl, switchActiveStack } from './utils'; +import { LSClient } from '../../services/LSClient'; +import { showInformationMessage } from '../../utils/notifications'; +import Panels from '../../common/panels'; +import { randomUUID } from 'crypto'; +import StackForm from './StackForm'; +import { traceError, traceInfo } from '../../common/log/logging'; + +/** + * Refreshes the stack view. + */ +const refreshStackView = async () => { + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Refreshing Stack View...', + cancellable: false, + }, + async progress => { + await StackDataProvider.getInstance().refresh(); + } + ); +}; + +/** + * Refreshes the active stack. + */ +const refreshActiveStack = async () => { + const statusBar = ZenMLStatusBar.getInstance(); + + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Refreshing Active Stack...', + cancellable: false, + }, + async progress => { + await statusBar.refreshActiveStack(); + } + ); +}; + +/** + * Renames the selected stack to a new name. + * + * @param node The stack to rename. + * @returns {Promise} Resolves after renaming the stack. + */ +const renameStack = async (node: StackTreeItem): Promise => { + const newStackName = await vscode.window.showInputBox({ prompt: 'Enter new stack name' }); + if (!newStackName) { + return; + } + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Renaming Stack...', + cancellable: false, + }, + async () => { + try { + const { label, id } = node; + const lsClient = LSClient.getInstance(); + const result = await lsClient.sendLsClientRequest('renameStack', [id, newStackName]); + if (result && 'error' in result) { + throw new Error(result.error); + } + showInformationMessage(`Stack ${label} successfully renamed to ${newStackName}.`); + await StackDataProvider.getInstance().refresh(); + } catch (error: any) { + if (error.response) { + vscode.window.showErrorMessage(`Failed to rename stack: ${error.response.data.message}`); + } else { + console.error('Failed to rename stack:', error); + vscode.window.showErrorMessage('Failed to rename stack'); + } + } + } + ); +}; + +/** + * Copies the selected stack to a new stack with a specified name. + * + * @param {StackTreeItem} node The stack to copy. + */ +const copyStack = async (node: StackTreeItem) => { + const newStackName = await vscode.window.showInputBox({ + prompt: 'Enter the name for the copied stack', + }); + if (!newStackName) { + return; + } + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Copying Stack...', + cancellable: false, + }, + async progress => { + try { + const lsClient = LSClient.getInstance(); + const result = await lsClient.sendLsClientRequest('copyStack', [node.id, newStackName]); + if ('error' in result && result.error) { + throw new Error(result.error); + } + showInformationMessage('Stack copied successfully.'); + await StackDataProvider.getInstance().refresh(); + } catch (error: any) { + if (error.response && error.response.data && error.response.data.message) { + vscode.window.showErrorMessage(`Failed to copy stack: ${error.response.data.message}`); + } else { + console.error('Failed to copy stack:', error); + vscode.window.showErrorMessage(`Failed to copy stack: ${error.message || error}`); + } + } + } + ); +}; + +/** + * Sets the selected stack as the active stack and stores it in the global context. + * + * @param {StackTreeItem} node The stack to activate. + * @returns {Promise} Resolves after setting the active stack. + */ +const setActiveStack = async (node: StackTreeItem): Promise => { + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Setting Active Stack...', + cancellable: false, + }, + async () => { + try { + const result = await switchActiveStack(node.id); + if (result) { + const { id, name } = result; + showInformationMessage(`Active stack set to: ${name}`); + } + } catch (error) { + console.log(error); + vscode.window.showErrorMessage(`Failed to set active stack: ${error}`); + } + } + ); +}; + +/** + * Opens the selected stack in the ZenML Dashboard in the browser + * + * @param {StackTreeItem} node The stack to open. + */ +const goToStackUrl = (node: StackTreeItem) => { + const url = getStackDashboardUrl(node.id); + + if (url) { + try { + const parsedUrl = vscode.Uri.parse(url); + + vscode.env.openExternal(parsedUrl); + vscode.window.showInformationMessage(`Opening: ${url}`); + } catch (error) { + console.log(error); + vscode.window.showErrorMessage(`Failed to open stack URL: ${error}`); + } + } +}; + +/** + * Opens the stack form webview panel to a form specific to registering a new + * stack. + */ +const registerStack = () => { + StackForm.getInstance().registerForm(); +}; + +/** + * Opens the stack form webview panel to a form specific to updating a specified stack. + * @param {StackTreeItem} node The specified stack to update. + */ +const updateStack = async (node: StackTreeItem) => { + const { id, label: name } = node; + const components: { [type: string]: string } = {}; + + node.children?.forEach(child => { + if (child instanceof StackComponentTreeItem) { + const { type, id } = (child as StackComponentTreeItem).component; + components[type] = id; + } + }); + + StackForm.getInstance().updateForm(id, name, components); +}; + +/** + * Deletes a specified stack. + * + * @param {StackTreeItem} node The Stack to delete + */ +const deleteStack = async (node: StackTreeItem) => { + const lsClient = LSClient.getInstance(); + + const answer = await vscode.window.showWarningMessage( + `Are you sure you want to delete ${node.label}? This cannot be undone.`, + { modal: true }, + 'Delete' + ); + + if (!answer) { + return; + } + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: `Deleting stack ${node.label}...`, + }, + async () => { + const { id } = node; + + try { + const resp = await lsClient.sendLsClientRequest('deleteStack', [id]); + + if ('error' in resp) { + throw resp.error; + } + + vscode.window.showInformationMessage(`${node.label} deleted`); + traceInfo(`${node.label} deleted`); + + StackDataProvider.getInstance().refresh(); + } catch (e) { + vscode.window.showErrorMessage(`Failed to delete component: ${e}`); + traceError(e); + console.error(e); + } + } + ); +}; + +export const stackCommands = { + refreshStackView, + refreshActiveStack, + renameStack, + copyStack, + setActiveStack, + goToStackUrl, + registerStack, + updateStack, + deleteStack, +}; diff --git a/src/commands/stack/registry.ts b/src/commands/stack/registry.ts new file mode 100644 index 00000000..2a6cef3f --- /dev/null +++ b/src/commands/stack/registry.ts @@ -0,0 +1,76 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { StackDataProvider, StackTreeItem } from '../../views/activityBar'; +import { stackCommands } from './cmds'; +import { registerCommand } from '../../common/vscodeapi'; +import { ZenExtension } from '../../services/ZenExtension'; +import { ExtensionContext, commands, window } from 'vscode'; +import { node } from 'webpack'; + +/** + * Registers stack-related commands for the extension. + * + * @param {ExtensionContext} context - The context in which the extension operates, used for registering commands and managing their lifecycle. + */ +export const registerStackCommands = (context: ExtensionContext) => { + const stackDataProvider = StackDataProvider.getInstance(); + try { + const registeredCommands = [ + registerCommand( + 'zenml.setStackItemsPerPage', + async () => await stackDataProvider.updateItemsPerPage() + ), + registerCommand('zenml.refreshStackView', async () => await stackCommands.refreshStackView()), + registerCommand( + 'zenml.refreshActiveStack', + async () => await stackCommands.refreshActiveStack() + ), + registerCommand('zenml.registerStack', async () => stackCommands.registerStack()), + registerCommand('zenml.updateStack', async (node: StackTreeItem) => + stackCommands.updateStack(node) + ), + registerCommand( + 'zenml.deleteStack', + async (node: StackTreeItem) => await stackCommands.deleteStack(node) + ), + registerCommand( + 'zenml.renameStack', + async (node: StackTreeItem) => await stackCommands.renameStack(node) + ), + registerCommand( + 'zenml.setActiveStack', + async (node: StackTreeItem) => await stackCommands.setActiveStack(node) + ), + registerCommand( + 'zenml.goToStackUrl', + async (node: StackTreeItem) => await stackCommands.goToStackUrl(node) + ), + registerCommand( + 'zenml.copyStack', + async (node: StackTreeItem) => await stackCommands.copyStack(node) + ), + registerCommand('zenml.nextStackPage', async () => stackDataProvider.goToNextPage()), + registerCommand('zenml.previousStackPage', async () => stackDataProvider.goToPreviousPage()), + ]; + + registeredCommands.forEach(cmd => { + context.subscriptions.push(cmd); + ZenExtension.commandDisposables.push(cmd); + }); + + commands.executeCommand('setContext', 'stackCommandsRegistered', true); + } catch (error) { + console.error('Error registering stack commands:', error); + commands.executeCommand('setContext', 'stackCommandsRegistered', false); + } +}; diff --git a/src/commands/stack/utils.ts b/src/commands/stack/utils.ts new file mode 100644 index 00000000..164e006b --- /dev/null +++ b/src/commands/stack/utils.ts @@ -0,0 +1,107 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as vscode from 'vscode'; +import { LSClient } from '../../services/LSClient'; +import { GetActiveStackResponse, SetActiveStackResponse } from '../../types/LSClientResponseTypes'; +import { showErrorMessage } from '../../utils/notifications'; +import { getZenMLServerUrl } from '../../utils/global'; + +/** + * Switches the active ZenML stack to the specified stack name. + * + * @param {string} stackNameOrId - The id or name of the ZenML stack to be activated. + * @returns {Promise<{id: string, name: string}>} A promise that resolves with the id and name of the newly activated stack, or undefined on error. + */ +export const switchActiveStack = async ( + stackNameOrId: string +): Promise<{ id: string; name: string } | undefined> => { + try { + const lsClient = LSClient.getInstance(); + const result = await lsClient.sendLsClientRequest('switchActiveStack', [ + stackNameOrId, + ]); + if (result && 'error' in result) { + console.log('Error in switchZenMLStack result', result); + throw new Error(result.error); + } + const { id, name } = result; + await storeActiveStack(id); + return { id, name }; + } catch (error: any) { + console.error(`Error setting active stack: ${error}`); + showErrorMessage(`Failed to set active stack: ${error.message}`); + } +}; + +/** + * Gets the id and name of the active ZenML stack. + * + * @returns {Promise<{id: string, name: string}>} A promise that resolves with the id and name of the active stack, or undefined on error; + */ +export const getActiveStack = async (): Promise<{ id: string; name: string } | undefined> => { + const lsClient = LSClient.getInstance(); + if (!lsClient.clientReady) { + return; + } + + try { + const result = await lsClient.sendLsClientRequest('getActiveStack'); + if (result && 'error' in result) { + throw new Error(result.error); + } + return result; + } catch (error: any) { + console.error(`Failed to get active stack information: ${error}`); + return undefined; + } +}; + +/** + * Stores the specified ZenML stack id in the global configuration. + * + * @param {string} id - The id of the ZenML stack to be stored. + * @returns {Promise} A promise that resolves when the stack information has been successfully stored. + */ +export const storeActiveStack = async (id: string): Promise => { + const config = vscode.workspace.getConfiguration('zenml'); + await config.update('activeStackId', id, vscode.ConfigurationTarget.Global); +}; + +export const getActiveStackIdFromConfig = (): string | undefined => { + const config = vscode.workspace.getConfiguration('zenml'); + return config.get('activeStackId'); +}; + +/** + * Gets the Dashboard URL for the corresponding ZenML stack + * + * @param {string} id - The id of the ZenML stack to be opened + * @returns {string} - The URL corresponding to the pipeline in the ZenML Dashboard + */ +export const getStackDashboardUrl = (id: string): string => { + const STACK_URL_STUB = 'SERVER_URL/workspaces/default/stacks/STACK_ID/configuration'; + const currentServerUrl = getZenMLServerUrl(); + + const stackUrl = STACK_URL_STUB.replace('SERVER_URL', currentServerUrl).replace('STACK_ID', id); + + return stackUrl; +}; + +const stackUtils = { + switchActiveStack, + getActiveStack, + storeActiveStack, + getStackDashboardUrl, +}; + +export default stackUtils; diff --git a/src/common/WebviewBase.ts b/src/common/WebviewBase.ts new file mode 100644 index 00000000..52530645 --- /dev/null +++ b/src/common/WebviewBase.ts @@ -0,0 +1,30 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as vscode from 'vscode'; + +/** + * Provides functionality to share extension context among classes that inherit + * from it. + */ +export default class WebviewBase { + protected static context: vscode.ExtensionContext | null = null; + + /** + * Sets the extension context so that descendant classes can correctly + * path to their resources + * @param {vscode.ExtensionContext} context ExtensionContext + */ + public static setContext(context: vscode.ExtensionContext) { + WebviewBase.context = context; + } +} diff --git a/src/common/api.ts b/src/common/api.ts new file mode 100644 index 00000000..90f96e93 --- /dev/null +++ b/src/common/api.ts @@ -0,0 +1,114 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { LSClient } from '../services/LSClient'; +import { + ComponentsListResponse, + Flavor, + FlavorListResponse, + StackComponent, +} from '../types/StackTypes'; + +let flavors: Flavor[] = []; + +/** + * Gets all component flavors and caches them + * @returns {Flavor[]} List of flavors + */ +export const getAllFlavors = async (): Promise => { + if (flavors.length > 0) { + return flavors; + } + const lsClient = LSClient.getInstance(); + + let [page, maxPage] = [0, 1]; + do { + page++; + const resp = await lsClient.sendLsClientRequest('listFlavors', [ + page, + 10000, + ]); + + if ('error' in resp) { + console.error(`Error retrieving flavors: ${resp.error}`); + throw new Error(`Error retrieving flavors: ${resp.error}`); + } + + maxPage = resp.total_pages; + flavors = flavors.concat(resp.items); + } while (page < maxPage); + return flavors; +}; + +/** + * Gets all flavors of a specified component type + * @param {string} type Type of component to filter by + * @returns {Flavor[]} List of flavors that match the component type filter + */ +export const getFlavorsOfType = async (type: string): Promise => { + const flavors = await getAllFlavors(); + return flavors.filter(flavor => flavor.type === type); +}; + +/** + * Gets a specific flavor + * @param {string} name The name of the flavor to get + * @returns {Flavor} The specified flavor. + */ +export const getFlavor = async (name: string): Promise => { + const flavors = await getAllFlavors(); + const flavor = flavors.find(flavor => flavor.name === name); + + if (!flavor) { + throw Error(`getFlavor: Flavor ${name} not found`); + } + + return flavor; +}; + +/** + * Gets all stack components + * @returns {object} Object containing all components keyed by each type. + */ +export const getAllStackComponents = async (): Promise<{ + [type: string]: StackComponent[]; +}> => { + const lsClient = LSClient.getInstance(); + let components: StackComponent[] = []; + let [page, maxPage] = [0, 1]; + + do { + page++; + const resp = await lsClient.sendLsClientRequest('listComponents', [ + page, + 10000, + ]); + + if ('error' in resp) { + console.error(`Error retrieving components: ${resp.error}`); + throw new Error(`Error retrieving components: ${resp.error}`); + } + + maxPage = resp.total_pages; + components = components.concat(resp.items); + } while (page < maxPage); + + const out: { [type: string]: StackComponent[] } = {}; + components.forEach(component => { + if (!(component.type in out)) { + out[component.type] = []; + } + out[component.type].push(component); + }); + + return out; +}; diff --git a/src/common/constants.ts b/src/common/constants.ts new file mode 100644 index 00000000..0eb1989a --- /dev/null +++ b/src/common/constants.ts @@ -0,0 +1,25 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as path from 'path'; + +export const EXTENSION_ID = 'ZenML.zenml-vscode'; +const folderName = path.basename(__dirname); +export const EXTENSION_ROOT_DIR = + folderName === 'common' ? path.dirname(path.dirname(__dirname)) : path.dirname(__dirname); +export const BUNDLED_PYTHON_SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR, 'bundled'); +export const SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, 'tool', `lsp_server.py`); +export const DEBUG_SERVER_SCRIPT_PATH = path.join( + BUNDLED_PYTHON_SCRIPTS_DIR, + 'tool', + `_debug_server.py` +); diff --git a/src/common/log/logging.ts b/src/common/log/logging.ts new file mode 100644 index 00000000..cbd1e152 --- /dev/null +++ b/src/common/log/logging.ts @@ -0,0 +1,70 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. + +import * as util from 'util'; +import { Disposable, LogOutputChannel } from 'vscode'; + +type Arguments = unknown[]; +class OutputChannelLogger { + constructor(private readonly channel: LogOutputChannel) {} + + public traceLog(...data: Arguments): void { + this.channel.appendLine(util.format(...data)); + } + + public traceError(...data: Arguments): void { + this.channel.error(util.format(...data)); + } + + public traceWarn(...data: Arguments): void { + this.channel.warn(util.format(...data)); + } + + public traceInfo(...data: Arguments): void { + this.channel.info(util.format(...data)); + } + + public traceVerbose(...data: Arguments): void { + this.channel.debug(util.format(...data)); + } +} + +let channel: OutputChannelLogger | undefined; +export function registerLogger(logChannel: LogOutputChannel): Disposable { + channel = new OutputChannelLogger(logChannel); + return { + dispose: () => { + channel = undefined; + }, + }; +} + +export function traceLog(...args: Arguments): void { + channel?.traceLog(...args); +} + +export function traceError(...args: Arguments): void { + channel?.traceError(...args); +} + +export function traceWarn(...args: Arguments): void { + channel?.traceWarn(...args); +} + +export function traceInfo(...args: Arguments): void { + channel?.traceInfo(...args); +} + +export function traceVerbose(...args: Arguments): void { + channel?.traceVerbose(...args); +} diff --git a/src/common/panels.ts b/src/common/panels.ts new file mode 100644 index 00000000..76365b9b --- /dev/null +++ b/src/common/panels.ts @@ -0,0 +1,113 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as vscode from 'vscode'; + +/** + * Handles creation and monitoring of webview panels. + */ +export default class Panels { + private static instance: Panels | undefined; + private openPanels: { [id: string]: vscode.WebviewPanel }; + + constructor() { + this.openPanels = {}; + } + + /** + * Retrieves a singleton instance of Panels + * @returns {Panels} The singleton instance + */ + public static getInstance(): Panels { + if (Panels.instance === undefined) { + Panels.instance = new Panels(); + } + return Panels.instance; + } + + /** + * Creates a webview panel + * @param {string} id ID of the webview panel to create + * @param {string} label Title of webview panel tab + * @param {vscode.WebviewPanelOptions & vscode.WebviewOptions} options + * Options applied to the webview panel + * @returns {vscode.WebviewPanel} The webview panel created + */ + public createPanel( + id: string, + label: string, + options?: vscode.WebviewPanelOptions & vscode.WebviewOptions + ) { + const panel = vscode.window.createWebviewPanel(id, label, vscode.ViewColumn.One, options); + panel.webview.html = this.getLoadingContent(); + + this.openPanels[id] = panel; + + panel.onDidDispose(() => { + this.deregisterPanel(id); + }, null); + + return panel; + } + + /** + * Gets existing webview panel + * @param {string} id ID of webview panel to retrieve. + * @param {boolean} forceSpinner Whether to change the html content or not + * @returns {vscode.WebviewPanel | undefined} The webview panel if it exists, + * else undefined + */ + public getPanel(id: string, forceSpinner: boolean = false): vscode.WebviewPanel | undefined { + const panel = this.openPanels[id]; + + if (panel && forceSpinner) { + panel.webview.html = this.getLoadingContent(); + } + + return panel; + } + + private deregisterPanel(id: string) { + delete this.openPanels[id]; + } + + private getLoadingContent(): string { + return ` + + + + + + + Loading + + + +
+ +`; + } +} diff --git a/src/common/python.ts b/src/common/python.ts new file mode 100644 index 00000000..a873f984 --- /dev/null +++ b/src/common/python.ts @@ -0,0 +1,148 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. + +/* eslint-disable @typescript-eslint/naming-convention */ +import { commands, Disposable, Event, EventEmitter, Uri } from 'vscode'; +import { traceError, traceLog } from './log/logging'; +import { PythonExtension, ResolvedEnvironment } from '@vscode/python-extension'; +export interface IInterpreterDetails { + path?: string[]; + resource?: Uri; +} + +const onDidChangePythonInterpreterEvent = new EventEmitter(); +export const onDidChangePythonInterpreter: Event = + onDidChangePythonInterpreterEvent.event; + +let _api: PythonExtension | undefined; +async function getPythonExtensionAPI(): Promise { + if (_api) { + return _api; + } + _api = await PythonExtension.api(); + return _api; +} + +/** + * Initialize the python extension. + * + * @param disposables - List of disposables to be disposed when the extension is deactivated. + */ +export async function initializePython(disposables: Disposable[]): Promise { + try { + const api = await getPythonExtensionAPI(); + + if (api) { + disposables.push( + api.environments.onDidChangeActiveEnvironmentPath(e => { + onDidChangePythonInterpreterEvent.fire({ path: [e.path], resource: e.resource?.uri }); + }) + ); + + traceLog('Waiting for interpreter from python extension.'); + onDidChangePythonInterpreterEvent.fire(await getInterpreterDetails()); + } + } catch (error) { + traceError('Error initializing python: ', error); + } +} + +/** + * Resolve the python interpreter. + * + * @param interpreter - The interpreter to resolve. + * @returns The resolved environment. + */ +export async function resolveInterpreter( + interpreter: string[] +): Promise { + const api = await getPythonExtensionAPI(); + return api?.environments.resolveEnvironment(interpreter[0]); +} + +/** + * Get the interpreter details. + * + * @param resource - The resource to get the interpreter details from. + * @returns The interpreter details. + */ +export async function getInterpreterDetails(resource?: Uri): Promise { + const api = await getPythonExtensionAPI(); + const environment = await api?.environments.resolveEnvironment( + api?.environments.getActiveEnvironmentPath(resource) + ); + if (environment?.executable.uri && checkVersion(environment)) { + return { path: [environment?.executable.uri.fsPath], resource }; + } + return { path: undefined, resource }; +} + +/** + * Get the path to the debugger. + * + * @returns The path to the debugger. + */ +export async function getDebuggerPath(): Promise { + const api = await getPythonExtensionAPI(); + return api?.debug.getDebuggerPackagePath(); +} + +/** + * Run a python extension command. + * + * @param command - The command to run. + * @param rest - The rest of the arguments. + * @returns The result of the command. + */ +export async function runPythonExtensionCommand(command: string, ...rest: any[]) { + await getPythonExtensionAPI(); + return await commands.executeCommand(command, ...rest); +} + +/** + * Check if the python version is supported. + * + * @param resolved - The resolved environment. + * @returns True if the version is supported, false otherwise. + */ +export function checkVersion(resolved: ResolvedEnvironment | undefined): boolean { + const version = resolved?.version; + if (version?.major === 3 && version?.minor >= 8) { + traceLog(`Python version ${version?.major}.${version?.minor}.${version?.micro} is supported.`); + return true; + } + traceError(`Python version ${version?.major}.${version?.minor} is not supported.`); + traceError(`Selected python path: ${resolved?.executable.uri?.fsPath}`); + traceError('Supported versions are 3.8 and above.'); + return false; +} + +/** + * Check if the python version is supported. + * + * @param resolvedEnv - The resolved environment. + * @returns An object with the result and an optional message. + */ +export function isPythonVersionSupported(resolvedEnv: ResolvedEnvironment | undefined): { + isSupported: boolean; + message?: string; +} { + const version = resolvedEnv?.version; + + if (version?.major === 3 && version?.minor >= 8) { + return { isSupported: true }; + } + + const errorMessage = `Unsupported Python ${version?.major}.${version?.minor}; requires >= 3.8.`; + return { isSupported: false, message: errorMessage }; +} diff --git a/src/common/server.ts b/src/common/server.ts new file mode 100644 index 00000000..b2862f8f --- /dev/null +++ b/src/common/server.ts @@ -0,0 +1,182 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as fsapi from 'fs-extra'; +import * as vscode from 'vscode'; +import { Disposable, env, l10n, LanguageStatusSeverity, LogOutputChannel } from 'vscode'; +import { State } from 'vscode-languageclient'; +import { + LanguageClient, + LanguageClientOptions, + RevealOutputChannelOn, + ServerOptions, +} from 'vscode-languageclient/node'; +import { EventBus } from '../services/EventBus'; +import { LSClient } from '../services/LSClient'; +import { ZenExtension } from '../services/ZenExtension'; +import { LSCLIENT_STATE_CHANGED } from '../utils/constants'; +import { toggleCommands } from '../utils/global'; +import { DEBUG_SERVER_SCRIPT_PATH, SERVER_SCRIPT_PATH } from './constants'; +import { traceError, traceInfo, traceVerbose } from './log/logging'; +import { getDebuggerPath } from './python'; +import { + getExtensionSettings, + getGlobalSettings, + getWorkspaceSettings, + ISettings, +} from './settings'; +import { updateStatus } from './status'; +import { getLSClientTraceLevel, getProjectRoot } from './utilities'; +import { isVirtualWorkspace } from './vscodeapi'; + +export type IInitOptions = { settings: ISettings[]; globalSettings: ISettings }; + +async function createServer( + settings: ISettings, + serverId: string, + serverName: string, + outputChannel: LogOutputChannel, + initializationOptions: IInitOptions +): Promise { + const command = settings.interpreter[0]; + // console.log('command is', command); + + const cwd = settings.cwd; + + // Set debugger path needed for debugging python code. + const newEnv = { ...process.env }; + const debuggerPath = await getDebuggerPath(); + const isDebugScript = await fsapi.pathExists(DEBUG_SERVER_SCRIPT_PATH); + if (newEnv.USE_DEBUGPY && debuggerPath) { + newEnv.DEBUGPY_PATH = debuggerPath; + } else { + newEnv.USE_DEBUGPY = 'False'; + } + + // Set import strategy + newEnv.LS_IMPORT_STRATEGY = settings.importStrategy; + + // Set notification type + newEnv.LS_SHOW_NOTIFICATION = settings.showNotifications; + + const args = + newEnv.USE_DEBUGPY === 'False' || !isDebugScript + ? settings.interpreter.slice(1).concat([SERVER_SCRIPT_PATH]) + : settings.interpreter.slice(1).concat([DEBUG_SERVER_SCRIPT_PATH]); + traceInfo(`Server run command: ${[command, ...args].join(' ')}`); + + const serverOptions: ServerOptions = { + command, + args, + options: { cwd, env: newEnv }, + }; + + // Options to control the language client + const clientOptions: LanguageClientOptions = { + // Register the server for python documents + documentSelector: isVirtualWorkspace() + ? [{ language: 'python' }] + : [ + { scheme: 'file', language: 'python' }, + { scheme: 'untitled', language: 'python' }, + { scheme: 'vscode-notebook', language: 'python' }, + { scheme: 'vscode-notebook-cell', language: 'python' }, + ], + outputChannel: outputChannel, + traceOutputChannel: outputChannel, + revealOutputChannelOn: RevealOutputChannelOn.Never, + initializationOptions: { + ...initializationOptions, + interpreter: settings.interpreter[0], + }, + }; + + return new LanguageClient(serverId, serverName, serverOptions, clientOptions); +} + +let _disposables: Disposable[] = []; +export async function restartServer( + workspaceSetting: ISettings +): Promise { + const lsClientInstance = LSClient.getInstance(); + const lsClient = lsClientInstance.getLanguageClient(); + if (lsClient) { + traceInfo(`Server: Stop requested`); + try { + await lsClient.stop(); + } catch (e) { + traceInfo(`Server: Stop failed - ${e}`, '\nContinuing to attempt to start'); + } + _disposables.forEach(d => d.dispose()); + _disposables = []; + } + updateStatus(undefined, LanguageStatusSeverity.Information, true); + + const newLSClient = await createServer( + workspaceSetting, + ZenExtension.serverId, + ZenExtension.serverName, + ZenExtension.outputChannel, + { + settings: await getExtensionSettings(ZenExtension.serverId, true), + globalSettings: await getGlobalSettings(ZenExtension.serverId, true), + } + ); + + lsClientInstance.updateClient(newLSClient); + + traceInfo(`Server: Start requested.`); + _disposables.push( + newLSClient.onDidChangeState(e => { + EventBus.getInstance().emit(LSCLIENT_STATE_CHANGED, e.newState); + switch (e.newState) { + case State.Stopped: + traceVerbose(`Server State: Stopped`); + break; + case State.Starting: + traceVerbose(`Server State: Starting`); + break; + case State.Running: + traceVerbose(`Server State: Running`); + updateStatus(undefined, LanguageStatusSeverity.Information, false); + break; + } + }) + ); + try { + await ZenExtension.lsClient.startLanguageClient(); + } catch (ex) { + updateStatus(l10n.t('Server failed to start.'), LanguageStatusSeverity.Error); + traceError(`Server: Start failed: ${ex}`); + } + await newLSClient.setTrace( + getLSClientTraceLevel(ZenExtension.outputChannel.logLevel, env.logLevel) + ); + return newLSClient; +} + +export async function runServer() { + await toggleCommands(false); + + const projectRoot = await getProjectRoot(); + const workspaceSetting = await getWorkspaceSettings(ZenExtension.serverId, projectRoot, true); + if (workspaceSetting.interpreter.length === 0) { + updateStatus( + vscode.l10n.t('Please select a Python interpreter.'), + vscode.LanguageStatusSeverity.Error + ); + traceError('Python interpreter missing. Please use Python 3.8 or greater.'); + return; + } + + await restartServer(workspaceSetting); +} diff --git a/src/common/settings.ts b/src/common/settings.ts new file mode 100644 index 00000000..5978ba3c --- /dev/null +++ b/src/common/settings.ts @@ -0,0 +1,177 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { + ConfigurationChangeEvent, + ConfigurationScope, + ConfigurationTarget, + WorkspaceConfiguration, + WorkspaceFolder, + workspace, +} from 'vscode'; +import { getInterpreterDetails } from './python'; +import { getConfiguration, getWorkspaceFolders } from './vscodeapi'; +import path from 'path'; +import * as fs from 'fs'; +import { PYTOOL_MODULE } from '../utils/constants'; +import { getProjectRoot } from './utilities'; + +export interface ISettings { + cwd: string; + workspace: string; + args: string[]; + path: string[]; + interpreter: string[]; + importStrategy: string; + showNotifications: string; +} + +export function getExtensionSettings( + namespace: string, + includeInterpreter?: boolean +): Promise { + return Promise.all( + getWorkspaceFolders().map(w => getWorkspaceSettings(namespace, w, includeInterpreter)) + ); +} + +function resolveVariables( + value: (string | { path: string })[], + workspace?: WorkspaceFolder +): string[] { + const substitutions = new Map(); + const home = process.env.HOME || process.env.USERPROFILE; + if (home) { + substitutions.set('${userHome}', home); + } + if (workspace) { + substitutions.set('${workspaceFolder}', workspace.uri.fsPath); + } + substitutions.set('${cwd}', process.cwd()); + getWorkspaceFolders().forEach(w => { + substitutions.set('${workspaceFolder:' + w.name + '}', w.uri.fsPath); + }); + + return value.map(item => { + // Check if item is an object and has a path property + if (typeof item === 'object' && 'path' in item) { + let path = item.path; + for (const [key, value] of substitutions) { + path = path.replace(key, value); + } + return path; + } else if (typeof item === 'string') { + // Item is a string, proceed as before + for (const [key, value] of substitutions) { + item = item.replace(key, value); + } + return item; + } else { + // Item is not a string or does not match the expected structure, log a warning or handle as needed + console.warn('Item does not match expected format:', item); + return ''; // or return a sensible default + } + }); +} + +export function getInterpreterFromSetting(namespace: string, scope?: ConfigurationScope) { + const config = getConfiguration(namespace, scope); + return config.get('interpreter'); +} + +export async function getWorkspaceSettings( + namespace: string, + workspace: WorkspaceFolder, + includeInterpreter?: boolean +): Promise { + const config = getConfiguration(namespace, workspace.uri); + + let interpreter: string[] = []; + if (includeInterpreter) { + interpreter = getInterpreterFromSetting(namespace, workspace) ?? []; + if (interpreter.length === 0) { + interpreter = (await getInterpreterDetails(workspace.uri)).path ?? []; + } + } + + const workspaceSetting = { + cwd: workspace.uri.fsPath, + workspace: workspace.uri.toString(), + args: resolveVariables(config.get(`args`) ?? [], workspace), + path: resolveVariables(config.get(`path`) ?? [], workspace), + interpreter: resolveVariables(interpreter, workspace), + importStrategy: config.get(`importStrategy`) ?? 'useBundled', + showNotifications: config.get(`showNotifications`) ?? 'off', + }; + + // console.log("WORKSPACE SETTINGS: ", workspaceSetting); + + return workspaceSetting; +} + +function getGlobalValue(config: WorkspaceConfiguration, key: string, defaultValue: T): T { + const inspect = config.inspect(key); + return inspect?.globalValue ?? inspect?.defaultValue ?? defaultValue; +} + +export async function getGlobalSettings( + namespace: string, + includeInterpreter?: boolean +): Promise { + const config = getConfiguration(namespace); + + let interpreter: string[] = []; + if (includeInterpreter) { + interpreter = getGlobalValue(config, 'interpreter', []); + if (interpreter === undefined || interpreter.length === 0) { + interpreter = (await getInterpreterDetails()).path ?? []; + } + } + + // const debugInterpreter = (await getInterpreterDetails()).path ?? []; + // console.log('Global Interpreter: ', debugInterpreter); + + const setting = { + cwd: process.cwd(), + workspace: process.cwd(), + args: getGlobalValue(config, 'args', []), + path: getGlobalValue(config, 'path', []), + interpreter: interpreter, + importStrategy: getGlobalValue(config, 'importStrategy', 'useBundled'), + showNotifications: getGlobalValue(config, 'showNotifications', 'off'), + }; + + // console.log("GLOBAL SETTINGS: ", setting); + + return setting; +} + +export function checkIfConfigurationChanged( + e: ConfigurationChangeEvent, + namespace: string +): boolean { + const settings = [ + `${namespace}.args`, + `${namespace}.path`, + `${namespace}.interpreter`, + `${namespace}.importStrategy`, + `${namespace}.showNotifications`, + ]; + const changed = settings.map(s => e.affectsConfiguration(s)); + return changed.includes(true); +} + +export async function getInterpreterFromWorkspaceSettings(): Promise { + const projectRoot = await getProjectRoot(); + const workspaceSettings = await getWorkspaceSettings(PYTOOL_MODULE, projectRoot, true); + return workspaceSettings.interpreter[0]; +} diff --git a/src/common/status.ts b/src/common/status.ts new file mode 100644 index 00000000..642ff8ab --- /dev/null +++ b/src/common/status.ts @@ -0,0 +1,45 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { LanguageStatusItem, Disposable, l10n, LanguageStatusSeverity } from 'vscode'; +import { createLanguageStatusItem } from './vscodeapi'; +import { Command } from 'vscode-languageclient'; +import { getDocumentSelector } from './utilities'; + +let _status: LanguageStatusItem | undefined; +export function registerLanguageStatusItem(id: string, name: string, command: string): Disposable { + _status = createLanguageStatusItem(id, getDocumentSelector()); + _status.name = name; + _status.text = name; + _status.command = Command.create(l10n.t('Open logs'), command); + + return { + dispose: () => { + _status?.dispose(); + _status = undefined; + }, + }; +} + +export function updateStatus( + status: string | undefined, + severity: LanguageStatusSeverity, + busy?: boolean, + detail?: string +): void { + if (_status) { + _status.text = status && status.length > 0 ? `${_status.name}: ${status}` : `${_status.name}`; + _status.severity = severity; + _status.busy = busy ?? false; + _status.detail = detail; + } +} diff --git a/src/common/utilities.ts b/src/common/utilities.ts new file mode 100644 index 00000000..5b365442 --- /dev/null +++ b/src/common/utilities.ts @@ -0,0 +1,89 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { DocumentSelector, LogLevel, Uri, WorkspaceFolder } from 'vscode'; +import { Trace } from 'vscode-jsonrpc/node'; +import { getWorkspaceFolders, isVirtualWorkspace } from './vscodeapi'; + +function logLevelToTrace(logLevel: LogLevel): Trace { + switch (logLevel) { + case LogLevel.Error: + case LogLevel.Warning: + case LogLevel.Info: + return Trace.Messages; + + case LogLevel.Debug: + case LogLevel.Trace: + return Trace.Verbose; + + case LogLevel.Off: + default: + return Trace.Off; + } +} + +export function getLSClientTraceLevel(channelLogLevel: LogLevel, globalLogLevel: LogLevel): Trace { + if (channelLogLevel === LogLevel.Off) { + return logLevelToTrace(globalLogLevel); + } + if (globalLogLevel === LogLevel.Off) { + return logLevelToTrace(channelLogLevel); + } + const level = logLevelToTrace( + channelLogLevel <= globalLogLevel ? channelLogLevel : globalLogLevel + ); + return level; +} + +export async function getProjectRoot(): Promise { + const workspaces: readonly WorkspaceFolder[] = getWorkspaceFolders(); + if (workspaces.length === 0) { + return { + uri: Uri.file(process.cwd()), + name: path.basename(process.cwd()), + index: 0, + }; + } else if (workspaces.length === 1) { + return workspaces[0]; + } else { + let rootWorkspace = workspaces[0]; + let root = undefined; + for (const w of workspaces) { + if (await fs.pathExists(w.uri.fsPath)) { + root = w.uri.fsPath; + rootWorkspace = w; + break; + } + } + + for (const w of workspaces) { + if (root && root.length > w.uri.fsPath.length && (await fs.pathExists(w.uri.fsPath))) { + root = w.uri.fsPath; + rootWorkspace = w; + } + } + return rootWorkspace; + } +} + +export function getDocumentSelector(): DocumentSelector { + return isVirtualWorkspace() + ? [{ language: 'python' }] + : [ + { scheme: 'file', language: 'python' }, + { scheme: 'untitled', language: 'python' }, + { scheme: 'vscode-notebook', language: 'python' }, + { scheme: 'vscode-notebook-cell', language: 'python' }, + ]; +} diff --git a/src/common/vscodeapi.ts b/src/common/vscodeapi.ts new file mode 100644 index 00000000..ff2463bd --- /dev/null +++ b/src/common/vscodeapi.ts @@ -0,0 +1,71 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + commands, + ConfigurationScope, + Disposable, + DocumentSelector, + languages, + LanguageStatusItem, + LogOutputChannel, + Uri, + window, + workspace, + WorkspaceConfiguration, + WorkspaceFolder, +} from 'vscode'; + +export function createOutputChannel(name: string): LogOutputChannel { + return window.createOutputChannel(name, { log: true }); +} + +export function getConfiguration( + config: string, + scope?: ConfigurationScope +): WorkspaceConfiguration { + return workspace.getConfiguration(config, scope); +} + +export function registerCommand( + command: string, + callback: (...args: any[]) => any, + thisArg?: any +): Disposable { + return commands.registerCommand(command, callback, thisArg); +} + +export const { onDidChangeConfiguration } = workspace; + +export function isVirtualWorkspace(): boolean { + const isVirtual = + workspace.workspaceFolders && workspace.workspaceFolders.every(f => f.uri.scheme !== 'file'); + return !!isVirtual; +} + +export function getWorkspaceFolders(): readonly WorkspaceFolder[] { + return workspace.workspaceFolders ?? []; +} + +export function getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined { + return workspace.getWorkspaceFolder(uri); +} + +export function createLanguageStatusItem( + id: string, + selector: DocumentSelector +): LanguageStatusItem { + return languages.createLanguageStatusItem(id, selector); +} diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 00000000..8bc37721 --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,68 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as vscode from 'vscode'; +import { EventBus } from './services/EventBus'; +import { LSClient } from './services/LSClient'; +import { ZenExtension } from './services/ZenExtension'; +import { refreshUIComponents } from './utils/refresh'; +import { EnvironmentDataProvider } from './views/activityBar/environmentView/EnvironmentDataProvider'; +import { registerEnvironmentCommands } from './commands/environment/registry'; +import { LSP_ZENML_CLIENT_INITIALIZED } from './utils/constants'; +import { toggleCommands } from './utils/global'; +import DagRenderer from './commands/pipelines/DagRender'; +import WebviewBase from './common/WebviewBase'; + +export async function activate(context: vscode.ExtensionContext) { + const eventBus = EventBus.getInstance(); + const lsClient = LSClient.getInstance(); + + const handleZenMLClientInitialized = async (isInitialized: boolean) => { + console.log('ZenML client initialized: ', isInitialized); + if (isInitialized) { + await toggleCommands(true); + await refreshUIComponents(); + } + }; + + eventBus.on(LSP_ZENML_CLIENT_INITIALIZED, handleZenMLClientInitialized); + + vscode.window.createTreeView('zenmlEnvironmentView', { + treeDataProvider: EnvironmentDataProvider.getInstance(), + }); + registerEnvironmentCommands(context); + + await ZenExtension.activate(context, lsClient); + + context.subscriptions.push( + new vscode.Disposable(() => { + eventBus.off(LSP_ZENML_CLIENT_INITIALIZED, handleZenMLClientInitialized); + }) + ); + + WebviewBase.setContext(context); +} + +/** + * Deactivates the ZenML extension. + * + * @returns {Promise} A promise that resolves to void. + */ +export async function deactivate(): Promise { + const lsClient = LSClient.getInstance().getLanguageClient(); + + if (lsClient) { + await lsClient.stop(); + EventBus.getInstance().emit('lsClientReady', false); + } + DagRenderer.getInstance()?.deactivate(); +} diff --git a/src/services/EventBus.ts b/src/services/EventBus.ts new file mode 100644 index 00000000..8abbe1f8 --- /dev/null +++ b/src/services/EventBus.ts @@ -0,0 +1,33 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { EventEmitter } from 'events'; + +export class EventBus extends EventEmitter { + private static instance: EventBus; + + constructor() { + super(); + } + + /** + * Retrieves the singleton instance of EventBus. + * + * @returns {EventBus} The singleton instance. + */ + public static getInstance(): EventBus { + if (!EventBus.instance) { + EventBus.instance = new EventBus(); + } + return EventBus.instance; + } +} diff --git a/src/services/LSClient.ts b/src/services/LSClient.ts new file mode 100644 index 00000000..1c1c4b2d --- /dev/null +++ b/src/services/LSClient.ts @@ -0,0 +1,264 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { ProgressLocation, commands, window } from 'vscode'; +import { LanguageClient } from 'vscode-languageclient/node'; +import { storeActiveStack } from '../commands/stack/utils'; +import { GenericLSClientResponse, VersionMismatchError } from '../types/LSClientResponseTypes'; +import { LSNotificationIsZenMLInstalled } from '../types/LSNotificationTypes'; +import { ConfigUpdateDetails } from '../types/ServerInfoTypes'; +import { + LSCLIENT_READY, + LSP_IS_ZENML_INSTALLED, + LSP_ZENML_CLIENT_INITIALIZED, + LSP_ZENML_SERVER_CHANGED, + LSP_ZENML_STACK_CHANGED, + PYTOOL_MODULE, + REFRESH_ENVIRONMENT_VIEW, +} from '../utils/constants'; +import { getZenMLServerUrl, updateServerUrlAndToken } from '../utils/global'; +import { debounce } from '../utils/refresh'; +import { EventBus } from './EventBus'; + +export class LSClient { + private static instance: LSClient | null = null; + private client: LanguageClient | null = null; + private eventBus: EventBus = EventBus.getInstance(); + public clientReady: boolean = false; + public isZenMLReady = false; + public localZenML: LSNotificationIsZenMLInstalled = { + is_installed: false, + version: '', + }; + + public restartLSPServerDebounced = debounce(async () => { + await commands.executeCommand(`${PYTOOL_MODULE}.restart`); + // await refreshUIComponents(); + }, 500); + + /** + * Sets up notification listeners for the language client. + * + * @returns void + */ + public setupNotificationListeners(): void { + if (this.client) { + this.client.onNotification(LSP_ZENML_SERVER_CHANGED, this.handleServerChanged.bind(this)); + this.client.onNotification(LSP_ZENML_STACK_CHANGED, this.handleStackChanged.bind(this)); + this.client.onNotification(LSP_IS_ZENML_INSTALLED, this.handleZenMLInstalled.bind(this)); + this.client.onNotification(LSP_ZENML_CLIENT_INITIALIZED, this.handleZenMLReady.bind(this)); + } + } + + /** + * Starts the language client. + * + * @returns A promise resolving to void. + */ + public async startLanguageClient(): Promise { + try { + if (this.client) { + await this.client.start(); + this.clientReady = true; + this.eventBus.emit(LSCLIENT_READY, true); + console.log('Language client started successfully.'); + } + } catch (error) { + console.error('Failed to start the language client:', error); + } + } + + /** + * Handles the zenml/isInstalled notification. + * + * @param params The installation status of ZenML. + */ + public handleZenMLInstalled(params: { is_installed: boolean; version?: string }): void { + console.log(`Received ${LSP_IS_ZENML_INSTALLED} notification: `, params.is_installed); + this.localZenML = { + is_installed: params.is_installed, + version: params.version || '', + }; + this.eventBus.emit(LSP_IS_ZENML_INSTALLED, this.localZenML); + this.eventBus.emit(REFRESH_ENVIRONMENT_VIEW); + } + + /** + * Handles the zenml/ready notification. + * + * @param params The ready status of ZenML. + * @returns A promise resolving to void. + */ + public async handleZenMLReady(params: { ready: boolean }): Promise { + console.log(`Received ${LSP_ZENML_CLIENT_INITIALIZED} notification: `, params.ready); + if (!params.ready) { + this.eventBus.emit(LSP_ZENML_CLIENT_INITIALIZED, false); + await commands.executeCommand('zenml.promptForInterpreter'); + } else { + this.eventBus.emit(LSP_ZENML_CLIENT_INITIALIZED, true); + } + this.isZenMLReady = params.ready; + this.eventBus.emit(REFRESH_ENVIRONMENT_VIEW); + } + + /** + * Handles the zenml/serverChanged notification. + * + * @param details The details of the server update. + */ + public async handleServerChanged(details: ConfigUpdateDetails): Promise { + if (this.isZenMLReady) { + console.log(`Received ${LSP_ZENML_SERVER_CHANGED} notification`); + + const currentServerUrl = getZenMLServerUrl(); + const { url, api_token } = details; + if (currentServerUrl !== url) { + window.withProgress( + { + location: ProgressLocation.Notification, + title: 'ZenML config change detected', + cancellable: false, + }, + async progress => { + await this.stopLanguageClient(); + await updateServerUrlAndToken(url, api_token); + this.restartLSPServerDebounced(); + } + ); + } + } + } + + /** + * Stops the language client. + * + * @returns A promise resolving to void. + */ + public async stopLanguageClient(): Promise { + this.clientReady = false; + try { + if (this.client) { + await this.client.stop(); + this.eventBus.emit(LSCLIENT_READY, false); + console.log('Language client stopped successfully.'); + } + } catch (error) { + console.error('Failed to stop the language client:', error); + } + } + + /** + * Handles the zenml/stackChanged notification. + * + * @param activeStackId The ID of the active stack. + * @returns A promise resolving to void. + */ + public async handleStackChanged(activeStackId: string): Promise { + console.log(`Received ${LSP_ZENML_STACK_CHANGED} notification:`, activeStackId); + await storeActiveStack(activeStackId); + this.eventBus.emit(LSP_ZENML_STACK_CHANGED, activeStackId); + } + + /** + * Sends a request to the language server. + * + * @param {string} command The command to send to the language server. + * @param {any[]} [args] The arguments to send with the command. + * @returns {Promise} A promise resolving to the response from the language server. + */ + public async sendLsClientRequest( + command: string, + args?: any[] + ): Promise { + if (!this.client || !this.clientReady) { + console.error(`${command}: LSClient is not ready yet.`); + return { error: 'LSClient is not ready yet.' } as T; + } + if (!this.isZenMLReady) { + console.error(`${command}: ZenML Client is not initialized yet.`); + return { error: 'ZenML Client is not initialized.' } as T; + } + try { + const result = await this.client.sendRequest('workspace/executeCommand', { + command: `${PYTOOL_MODULE}.${command}`, + arguments: args, + }); + return result as T; + } catch (error: any) { + const errorMessage = error.message; + console.error(`Failed to execute command ${command}:`, errorMessage || error); + if (errorMessage.includes('ValidationError') || errorMessage.includes('RuntimeError')) { + return this.handleKnownErrors(error); + } + return { error: errorMessage } as T; + } + } + + private handleKnownErrors(error: any): T { + let errorType = 'Error'; + let serverVersion = 'N/A'; + let errorMessage = error.message; + let newErrorMessage = ''; + const versionRegex = /\b\d+\.\d+\.\d+\b/; + + if (errorMessage.includes('ValidationError')) { + errorType = 'ValidationError'; + } else if (errorMessage.includes('RuntimeError')) { + errorType = 'RuntimeError'; + if (errorMessage.includes('revision identified by')) { + const matches = errorMessage.match(versionRegex); + if (matches) { + serverVersion = matches[0]; + newErrorMessage = `Can't locate revision identified by ${serverVersion}`; + } + } + } + + return { + error: errorType, + message: newErrorMessage || errorMessage, + clientVersion: this.localZenML.version || 'N/A', + serverVersion, + } as T; + } + + /** + * Updates the language client. + * + * @param {LanguageClient} updatedCLient The new language client. + */ + public updateClient(updatedCLient: LanguageClient): void { + this.client = updatedCLient; + this.setupNotificationListeners(); + } + + /** + * Gets the language client. + * + * @returns {LanguageClient | null} The language client. + */ + public getLanguageClient(): LanguageClient | null { + return this.client; + } + + /** + * Retrieves the singleton instance of LSClient. + * + * @returns {LSClient} The singleton instance. + */ + public static getInstance(): LSClient { + if (!this.instance) { + this.instance = new LSClient(); + } + return this.instance; + } +} diff --git a/src/services/ZenExtension.ts b/src/services/ZenExtension.ts new file mode 100644 index 00000000..43a0f8e7 --- /dev/null +++ b/src/services/ZenExtension.ts @@ -0,0 +1,266 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as vscode from 'vscode'; + +import { registerPipelineCommands } from '../commands/pipelines/registry'; +import { registerServerCommands } from '../commands/server/registry'; +import { registerStackCommands } from '../commands/stack/registry'; +import { EXTENSION_ROOT_DIR } from '../common/constants'; +import { registerLogger, traceLog, traceVerbose } from '../common/log/logging'; +import { + IInterpreterDetails, + initializePython, + isPythonVersionSupported, + onDidChangePythonInterpreter, + resolveInterpreter, +} from '../common/python'; +import { runServer } from '../common/server'; +import { checkIfConfigurationChanged, getInterpreterFromSetting } from '../common/settings'; +import { registerLanguageStatusItem } from '../common/status'; +import { getLSClientTraceLevel } from '../common/utilities'; +import { + createOutputChannel, + onDidChangeConfiguration, + registerCommand, +} from '../common/vscodeapi'; +import { refreshUIComponents } from '../utils/refresh'; +import { PipelineDataProvider, ServerDataProvider, StackDataProvider } from '../views/activityBar'; +import ZenMLStatusBar from '../views/statusBar'; +import { LSClient } from './LSClient'; +import { toggleCommands } from '../utils/global'; +import { PanelDataProvider } from '../views/panel/panelView/PanelDataProvider'; +import { ComponentDataProvider } from '../views/activityBar/componentView/ComponentDataProvider'; +import { registerComponentCommands } from '../commands/components/registry'; + +export interface IServerInfo { + name: string; + module: string; +} + +export class ZenExtension { + private static context: vscode.ExtensionContext; + static commandDisposables: vscode.Disposable[] = []; + static viewDisposables: vscode.Disposable[] = []; + public static lsClient: LSClient; + public static outputChannel: vscode.LogOutputChannel; + public static serverId: string; + public static serverName: string; + private static viewsAndCommandsSetup = false; + public static interpreterCheckInProgress = false; + + private static dataProviders = new Map>([ + ['zenmlServerView', ServerDataProvider.getInstance()], + ['zenmlStackView', StackDataProvider.getInstance()], + ['zenmlComponentView', ComponentDataProvider.getInstance()], + ['zenmlPipelineView', PipelineDataProvider.getInstance()], + ['zenmlPanelView', PanelDataProvider.getInstance()], + ]); + + private static registries = [ + registerServerCommands, + registerStackCommands, + registerComponentCommands, + registerPipelineCommands, + ]; + + /** + * Initializes the extension services and saves the context for reuse. + * + * @param context The extension context provided by VS Code on activation. + */ + static async activate(context: vscode.ExtensionContext, lsClient: LSClient): Promise { + this.context = context; + this.lsClient = lsClient; + const serverDefaults = this.loadServerDefaults(); + this.serverName = serverDefaults.name; + this.serverId = serverDefaults.module; + + this.setupLoggingAndTrace(); + this.subscribeToCoreEvents(); + this.deferredInitialize(); + } + + /** + * Deferred initialization tasks to be run after initializing other tasks. + */ + static deferredInitialize(): void { + setImmediate(async () => { + const interpreter = getInterpreterFromSetting(this.serverId); + if (interpreter === undefined || interpreter.length === 0) { + traceLog(`Python extension loading`); + await initializePython(this.context.subscriptions); + traceLog(`Python extension loaded`); + } else { + await runServer(); + } + await this.setupViewsAndCommands(); + }); + } + + /** + * Sets up the views and commands for the ZenML extension. + */ + static async setupViewsAndCommands(): Promise { + if (this.viewsAndCommandsSetup) { + console.log('Views and commands have already been set up. Refreshing views...'); + await toggleCommands(true); + return; + } + + const zenmlStatusBar = ZenMLStatusBar.getInstance(); + zenmlStatusBar.registerCommands(); + + this.dataProviders.forEach((provider, viewId) => { + const view = vscode.window.createTreeView(viewId, { treeDataProvider: provider }); + this.viewDisposables.push(view); + }); + this.registries.forEach(register => register(this.context)); + await toggleCommands(true); + this.viewsAndCommandsSetup = true; + } + + /** + * Registers command and configuration event handlers to the extension context. + */ + private static subscribeToCoreEvents(): void { + this.context.subscriptions.push( + onDidChangePythonInterpreter(async (interpreterDetails: IInterpreterDetails) => { + this.interpreterCheckInProgress = true; + if (interpreterDetails.path) { + const resolvedEnv = await resolveInterpreter(interpreterDetails.path); + const { isSupported, message } = isPythonVersionSupported(resolvedEnv); + if (!isSupported) { + vscode.window.showErrorMessage(`Interpreter not supported: ${message}`); + this.interpreterCheckInProgress = false; + return; + } + await runServer(); + if (!this.lsClient.isZenMLReady) { + console.log('ZenML Client is not initialized yet.'); + await this.promptForPythonInterpreter(); + } else { + vscode.window.showInformationMessage('🚀 ZenML installation found. Ready to use.'); + await refreshUIComponents(); + } + } + this.interpreterCheckInProgress = false; + }), + registerCommand(`${this.serverId}.showLogs`, async () => { + this.outputChannel.show(); + }), + onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => { + if (checkIfConfigurationChanged(e, this.serverId)) { + console.log('Configuration changed, restarting LSP server...', e); + await runServer(); + } + }), + registerCommand(`${this.serverId}.restart`, async () => { + await runServer(); + }), + registerCommand(`zenml.promptForInterpreter`, async () => { + if (!this.interpreterCheckInProgress && !this.lsClient.isZenMLReady) { + await this.promptForPythonInterpreter(); + } + }), + registerLanguageStatusItem(this.serverId, this.serverName, `${this.serverId}.showLogs`) + ); + } + + /** + * Prompts the user to select a Python interpreter. + * + * @returns {Promise} A promise that resolves to void. + */ + static async promptForPythonInterpreter(): Promise { + if (this.interpreterCheckInProgress) { + console.log('Interpreter check already in progress. Skipping prompt.'); + return; + } + if (this.lsClient.isZenMLReady) { + console.log('ZenML is already installed, no need to prompt for interpreter.'); + return; + } + try { + const selected = await vscode.window.showInformationMessage( + 'ZenML not found with the current Python interpreter. Would you like to select a different interpreter?', + 'Select Interpreter', + 'Cancel' + ); + if (selected === 'Select Interpreter') { + await vscode.commands.executeCommand('python.setInterpreter'); + console.log('Interpreter selection completed.'); + } else { + console.log('Interpreter selection cancelled.'); + } + } catch (err) { + console.error('Error selecting Python interpreter:', err); + } + } + + /** + * Initializes the outputChannel and logging for the ZenML extension. + */ + private static setupLoggingAndTrace(): void { + this.outputChannel = createOutputChannel(this.serverName); + + this.context.subscriptions.push(this.outputChannel, registerLogger(this.outputChannel)); + const changeLogLevel = async (c: vscode.LogLevel, g: vscode.LogLevel) => { + const level = getLSClientTraceLevel(c, g); + const lsClient = LSClient.getInstance().getLanguageClient(); + await lsClient?.setTrace(level); + }; + + this.context.subscriptions.push( + this.outputChannel.onDidChangeLogLevel( + async e => await changeLogLevel(e, vscode.env.logLevel) + ), + vscode.env.onDidChangeLogLevel( + async e => await changeLogLevel(this.outputChannel.logLevel, e) + ) + ); + + traceLog(`Name: ${this.serverName}`); + traceLog(`Module: ${this.serverId}`); + traceVerbose( + `Full Server Info: ${JSON.stringify({ name: this.serverName, module: this.serverId })}` + ); + } + + /** + * Loads the server defaults from the package.json file. + * + * @returns {IServerInfo} The server defaults. + */ + private static loadServerDefaults(): IServerInfo { + const packageJson = path.join(EXTENSION_ROOT_DIR, 'package.json'); + const content = fs.readFileSync(packageJson).toString(); + const config = JSON.parse(content); + return config.serverInfo as IServerInfo; + } + + /** + * Deactivates ZenML features when requirements not met. + * + * @returns {Promise} A promise that resolves to void. + */ + static async deactivateFeatures(): Promise { + this.commandDisposables.forEach(disposable => disposable.dispose()); + this.commandDisposables = []; + + this.viewDisposables.forEach(disposable => disposable.dispose()); + this.viewDisposables = []; + console.log('Features deactivated due to unmet requirements.'); + } +} diff --git a/src/test/python_tests/__init__.py b/src/test/python_tests/__init__.py new file mode 100644 index 00000000..d83bd1ae --- /dev/null +++ b/src/test/python_tests/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. diff --git a/src/test/python_tests/lsp_test_client/__init__.py b/src/test/python_tests/lsp_test_client/__init__.py new file mode 100644 index 00000000..d83bd1ae --- /dev/null +++ b/src/test/python_tests/lsp_test_client/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. diff --git a/src/test/python_tests/lsp_test_client/constants.py b/src/test/python_tests/lsp_test_client/constants.py new file mode 100644 index 00000000..a2d76fae --- /dev/null +++ b/src/test/python_tests/lsp_test_client/constants.py @@ -0,0 +1,21 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +""" +Constants for use with tests. +""" +import pathlib + +TEST_ROOT = pathlib.Path(__file__).parent.parent +PROJECT_ROOT = TEST_ROOT.parent.parent.parent +TEST_DATA = TEST_ROOT / "test_data" diff --git a/src/test/python_tests/lsp_test_client/defaults.py b/src/test/python_tests/lsp_test_client/defaults.py new file mode 100644 index 00000000..c242decf --- /dev/null +++ b/src/test/python_tests/lsp_test_client/defaults.py @@ -0,0 +1,230 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +""" +Default initialize request params. +""" + +import os + +from .constants import PROJECT_ROOT +from .utils import as_uri, get_initialization_options + +VSCODE_DEFAULT_INITIALIZE = { + "processId": os.getpid(), + "clientInfo": {"name": "vscode", "version": "1.45.0"}, + "rootPath": str(PROJECT_ROOT), + "rootUri": as_uri(str(PROJECT_ROOT)), + "capabilities": { + "workspace": { + "applyEdit": True, + "workspaceEdit": { + "documentChanges": True, + "resourceOperations": ["create", "rename", "delete"], + "failureHandling": "textOnlyTransactional", + }, + "didChangeConfiguration": {"dynamicRegistration": True}, + "didChangeWatchedFiles": {"dynamicRegistration": True}, + "symbol": { + "dynamicRegistration": True, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + ] + }, + "tagSupport": {"valueSet": [1]}, + }, + "executeCommand": {"dynamicRegistration": True}, + "configuration": True, + "workspaceFolders": True, + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": True, + "versionSupport": False, + "tagSupport": {"valueSet": [1, 2]}, + "complexDiagnosticCodeSupport": True, + }, + "synchronization": { + "dynamicRegistration": True, + "willSave": True, + "willSaveWaitUntil": True, + "didSave": True, + }, + "completion": { + "dynamicRegistration": True, + "contextSupport": True, + "completionItem": { + "snippetSupport": True, + "commitCharactersSupport": True, + "documentationFormat": ["markdown", "plaintext"], + "deprecatedSupport": True, + "preselectSupport": True, + "tagSupport": {"valueSet": [1]}, + "insertReplaceSupport": True, + }, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + ] + }, + }, + "hover": { + "dynamicRegistration": True, + "contentFormat": ["markdown", "plaintext"], + }, + "signatureHelp": { + "dynamicRegistration": True, + "signatureInformation": { + "documentationFormat": ["markdown", "plaintext"], + "parameterInformation": {"labelOffsetSupport": True}, + }, + "contextSupport": True, + }, + "definition": {"dynamicRegistration": True, "linkSupport": True}, + "references": {"dynamicRegistration": True}, + "documentHighlight": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + ] + }, + "hierarchicalDocumentSymbolSupport": True, + "tagSupport": {"valueSet": [1]}, + }, + "codeAction": { + "dynamicRegistration": True, + "isPreferredSupport": True, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports", + ] + } + }, + }, + "codeLens": {"dynamicRegistration": True}, + "formatting": {"dynamicRegistration": True}, + "rangeFormatting": {"dynamicRegistration": True}, + "onTypeFormatting": {"dynamicRegistration": True}, + "rename": {"dynamicRegistration": True, "prepareSupport": True}, + "documentLink": { + "dynamicRegistration": True, + "tooltipSupport": True, + }, + "typeDefinition": { + "dynamicRegistration": True, + "linkSupport": True, + }, + "implementation": { + "dynamicRegistration": True, + "linkSupport": True, + }, + "colorProvider": {"dynamicRegistration": True}, + "foldingRange": { + "dynamicRegistration": True, + "rangeLimit": 5000, + "lineFoldingOnly": True, + }, + "declaration": {"dynamicRegistration": True, "linkSupport": True}, + "selectionRange": {"dynamicRegistration": True}, + }, + "window": {"workDoneProgress": True}, + }, + "trace": "verbose", + "workspaceFolders": [{"uri": as_uri(str(PROJECT_ROOT)), "name": "my_project"}], + "initializationOptions": get_initialization_options(), +} diff --git a/src/test/python_tests/lsp_test_client/session.py b/src/test/python_tests/lsp_test_client/session.py new file mode 100644 index 00000000..c895772c --- /dev/null +++ b/src/test/python_tests/lsp_test_client/session.py @@ -0,0 +1,224 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +""" +LSP session client for testing. +""" + +import os +import subprocess +import sys +from concurrent.futures import Future, ThreadPoolExecutor +from threading import Event + +from pyls_jsonrpc.dispatchers import MethodDispatcher +from pyls_jsonrpc.endpoint import Endpoint +from pyls_jsonrpc.streams import JsonRpcStreamReader, JsonRpcStreamWriter + +from .constants import PROJECT_ROOT +from .defaults import VSCODE_DEFAULT_INITIALIZE + +LSP_EXIT_TIMEOUT = 5000 + + +PUBLISH_DIAGNOSTICS = "textDocument/publishDiagnostics" +WINDOW_LOG_MESSAGE = "window/logMessage" +WINDOW_SHOW_MESSAGE = "window/showMessage" + + +# pylint: disable=too-many-instance-attributes +class LspSession(MethodDispatcher): + """Send and Receive messages over LSP as a test LS Client.""" + + def __init__(self, cwd=None, script=None): + self.cwd = cwd if cwd else os.getcwd() + # pylint: disable=consider-using-with + self._thread_pool = ThreadPoolExecutor() + self._sub = None + self._writer = None + self._reader = None + self._endpoint = None + self._notification_callbacks = {} + self.script = ( + script if script else (PROJECT_ROOT / "bundled" / "tool" / "lsp_server.py") + ) + + def __enter__(self): + """Context manager entrypoint. + + shell=True needed for pytest-cov to work in subprocess. + """ + # pylint: disable=consider-using-with + self._sub = subprocess.Popen( + [sys.executable, str(self.script)], + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + bufsize=0, + cwd=self.cwd, + env=os.environ, + shell="WITH_COVERAGE" in os.environ, + ) + + self._writer = JsonRpcStreamWriter(os.fdopen(self._sub.stdin.fileno(), "wb")) + self._reader = JsonRpcStreamReader(os.fdopen(self._sub.stdout.fileno(), "rb")) + + dispatcher = { + PUBLISH_DIAGNOSTICS: self._publish_diagnostics, + WINDOW_SHOW_MESSAGE: self._window_show_message, + WINDOW_LOG_MESSAGE: self._window_log_message, + } + self._endpoint = Endpoint(dispatcher, self._writer.write) + self._thread_pool.submit(self._reader.listen, self._endpoint.consume) + return self + + def __exit__(self, typ, value, _tb): + self.shutdown(True) + try: + self._sub.terminate() + except Exception: # pylint:disable=broad-except + pass + self._endpoint.shutdown() + self._thread_pool.shutdown() + + def initialize( + self, + initialize_params=None, + process_server_capabilities=None, + ): + """Sends the initialize request to LSP server.""" + if initialize_params is None: + initialize_params = VSCODE_DEFAULT_INITIALIZE + server_initialized = Event() + + def _after_initialize(fut): + if process_server_capabilities: + process_server_capabilities(fut.result()) + self.initialized() + server_initialized.set() + + self._send_request( + "initialize", + params=( + initialize_params + if initialize_params is not None + else VSCODE_DEFAULT_INITIALIZE + ), + handle_response=_after_initialize, + ) + + server_initialized.wait() + + def initialized(self, initialized_params=None): + """Sends the initialized notification to LSP server.""" + self._endpoint.notify("initialized", initialized_params or {}) + + def shutdown(self, should_exit, exit_timeout=LSP_EXIT_TIMEOUT): + """Sends the shutdown request to LSP server.""" + + def _after_shutdown(_): + if should_exit: + self.exit_lsp(exit_timeout) + + self._send_request("shutdown", handle_response=_after_shutdown) + + def exit_lsp(self, exit_timeout=LSP_EXIT_TIMEOUT): + """Handles LSP server process exit.""" + self._endpoint.notify("exit") + assert self._sub.wait(exit_timeout) == 0 + + def notify_did_change(self, did_change_params): + """Sends did change notification to LSP Server.""" + self._send_notification("textDocument/didChange", params=did_change_params) + + def notify_did_save(self, did_save_params): + """Sends did save notification to LSP Server.""" + self._send_notification("textDocument/didSave", params=did_save_params) + + def notify_did_open(self, did_open_params): + """Sends did open notification to LSP Server.""" + self._send_notification("textDocument/didOpen", params=did_open_params) + + def notify_did_close(self, did_close_params): + """Sends did close notification to LSP Server.""" + self._send_notification("textDocument/didClose", params=did_close_params) + + def text_document_formatting(self, formatting_params): + """Sends text document references request to LSP server.""" + fut = self._send_request("textDocument/formatting", params=formatting_params) + return fut.result() + + def text_document_code_action(self, code_action_params): + """Sends text document code actions request to LSP server.""" + fut = self._send_request("textDocument/codeAction", params=code_action_params) + return fut.result() + + def code_action_resolve(self, code_action_resolve_params): + """Sends text document code actions resolve request to LSP server.""" + fut = self._send_request( + "codeAction/resolve", params=code_action_resolve_params + ) + return fut.result() + + def set_notification_callback(self, notification_name, callback): + """Set custom LS notification handler.""" + self._notification_callbacks[notification_name] = callback + + def get_notification_callback(self, notification_name): + """Gets callback if set or default callback for a given LS + notification.""" + try: + return self._notification_callbacks[notification_name] + except KeyError: + + def _default_handler(_params): + """Default notification handler.""" + + return _default_handler + + def _publish_diagnostics(self, publish_diagnostics_params): + """Internal handler for text document publish diagnostics.""" + return self._handle_notification( + PUBLISH_DIAGNOSTICS, publish_diagnostics_params + ) + + def _window_log_message(self, window_log_message_params): + """Internal handler for window log message.""" + return self._handle_notification(WINDOW_LOG_MESSAGE, window_log_message_params) + + def _window_show_message(self, window_show_message_params): + """Internal handler for window show message.""" + return self._handle_notification( + WINDOW_SHOW_MESSAGE, window_show_message_params + ) + + def _handle_notification(self, notification_name, params): + """Internal handler for notifications.""" + fut = Future() + + def _handler(): + callback = self.get_notification_callback(notification_name) + callback(params) + fut.set_result(None) + + self._thread_pool.submit(_handler) + return fut + + def _send_request(self, name, params=None, handle_response=lambda f: f.done()): + """Sends {name} request to the LSP server.""" + fut = self._endpoint.request(name, params) + fut.add_done_callback(handle_response) + return fut + + def _send_notification(self, name, params=None): + """Sends {name} notification to the LSP server.""" + self._endpoint.notify(name, params) diff --git a/src/test/python_tests/lsp_test_client/utils.py b/src/test/python_tests/lsp_test_client/utils.py new file mode 100644 index 00000000..509b63de --- /dev/null +++ b/src/test/python_tests/lsp_test_client/utils.py @@ -0,0 +1,84 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +""" +Utility functions for use with tests. +""" +import json +import os +import pathlib +import platform +from random import choice + +from .constants import PROJECT_ROOT + + +def normalizecase(path: str) -> str: + """Fixes 'file' uri or path case for easier testing in windows.""" + if platform.system() == "Windows": + return path.lower() + return path + + +def as_uri(path: str) -> str: + """Return 'file' uri as string.""" + return normalizecase(pathlib.Path(path).as_uri()) + + +class PythonFile: + """Create python file on demand for testing.""" + + def __init__(self, contents, root): + self.contents = contents + self.basename = "".join( + choice("abcdefghijklmnopqrstuvwxyz") if i < 8 else ".py" for i in range(9) + ) + self.fullpath = os.path.join(root, self.basename) + + def __enter__(self): + """Creates a python file for testing.""" + with open(self.fullpath, "w", encoding="utf8") as py_file: + py_file.write(self.contents) + return self + + def __exit__(self, typ, value, _tb): + """Cleans up and deletes the python file.""" + os.unlink(self.fullpath) + + +def get_server_info_defaults(): + """Returns server info from package.json""" + package_json_path = PROJECT_ROOT / "package.json" + package_json = json.loads(package_json_path.read_text()) + return package_json["serverInfo"] + + +def get_initialization_options(): + """Returns initialization options from package.json""" + package_json_path = PROJECT_ROOT / "package.json" + package_json = json.loads(package_json_path.read_text()) + + server_info = package_json["serverInfo"] + server_id = server_info["module"] + + properties = package_json["contributes"]["configuration"]["properties"] + setting = {} + for prop in properties: + name = prop[len(server_id) + 1 :] + value = properties[prop]["default"] + setting[name] = value + + setting["workspace"] = as_uri(str(PROJECT_ROOT)) + setting["interpreter"] = [] + + return {"settings": [setting]} diff --git a/src/test/python_tests/requirements.in b/src/test/python_tests/requirements.in new file mode 100644 index 00000000..15bbd40d --- /dev/null +++ b/src/test/python_tests/requirements.in @@ -0,0 +1,14 @@ +# This file is used to generate ./src/test/python_tests/requirements.txt. +# NOTE: +# Use Python 3.8 or greater which ever is the minimum version of the python +# you plan on supporting when creating the environment or using pip-tools. +# Only run the commands below to manully upgrade packages in requirements.txt: +# 1) python -m pip install pip-tools +# 2) pip-compile --generate-hashes --upgrade ./src/test/python_tests/requirements.in +# If you are using nox commands to setup or build package you don't need to +# run the above commands manually. + +# Packages needed by the testing framework. +pytest +PyHamcrest +python-jsonrpc-server diff --git a/src/test/python_tests/requirements.txt b/src/test/python_tests/requirements.txt new file mode 100644 index 00000000..d9003d0f --- /dev/null +++ b/src/test/python_tests/requirements.txt @@ -0,0 +1,118 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --generate-hashes ./src/test/python_tests/requirements.in +# +exceptiongroup==1.2.1 \ + --hash=sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad \ + --hash=sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16 + # via pytest +iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + # via pytest +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 + # via pytest +pluggy==1.5.0 \ + --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ + --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 + # via pytest +pyhamcrest==2.1.0 \ + --hash=sha256:c6acbec0923d0cb7e72c22af1926f3e7c97b8e8d69fc7498eabacaf7c975bd9c \ + --hash=sha256:f6913d2f392e30e0375b3ecbd7aee79e5d1faa25d345c8f4ff597665dcac2587 + # via -r ./src/test/python_tests/requirements.in +pytest==8.2.2 \ + --hash=sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343 \ + --hash=sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977 + # via -r ./src/test/python_tests/requirements.in +python-jsonrpc-server==0.4.0 \ + --hash=sha256:62c543e541f101ec5b57dc654efc212d2c2e3ea47ff6f54b2e7dcb36ecf20595 \ + --hash=sha256:e5a908ff182e620aac07db5f57887eeb0afe33993008f57dc1b85b594cea250c + # via -r ./src/test/python_tests/requirements.in +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f + # via pytest +ujson==5.10.0 \ + --hash=sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e \ + --hash=sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b \ + --hash=sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6 \ + --hash=sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7 \ + --hash=sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9 \ + --hash=sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd \ + --hash=sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569 \ + --hash=sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f \ + --hash=sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51 \ + --hash=sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20 \ + --hash=sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1 \ + --hash=sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf \ + --hash=sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc \ + --hash=sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e \ + --hash=sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a \ + --hash=sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539 \ + --hash=sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27 \ + --hash=sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165 \ + --hash=sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126 \ + --hash=sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1 \ + --hash=sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816 \ + --hash=sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64 \ + --hash=sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8 \ + --hash=sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e \ + --hash=sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287 \ + --hash=sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3 \ + --hash=sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb \ + --hash=sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0 \ + --hash=sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043 \ + --hash=sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557 \ + --hash=sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e \ + --hash=sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21 \ + --hash=sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d \ + --hash=sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd \ + --hash=sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0 \ + --hash=sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337 \ + --hash=sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753 \ + --hash=sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804 \ + --hash=sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f \ + --hash=sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f \ + --hash=sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5 \ + --hash=sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5 \ + --hash=sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1 \ + --hash=sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00 \ + --hash=sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2 \ + --hash=sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050 \ + --hash=sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e \ + --hash=sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4 \ + --hash=sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8 \ + --hash=sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996 \ + --hash=sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6 \ + --hash=sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1 \ + --hash=sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f \ + --hash=sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1 \ + --hash=sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4 \ + --hash=sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b \ + --hash=sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88 \ + --hash=sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518 \ + --hash=sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5 \ + --hash=sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770 \ + --hash=sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4 \ + --hash=sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a \ + --hash=sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76 \ + --hash=sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe \ + --hash=sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988 \ + --hash=sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1 \ + --hash=sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5 \ + --hash=sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b \ + --hash=sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7 \ + --hash=sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8 \ + --hash=sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc \ + --hash=sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a \ + --hash=sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720 \ + --hash=sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3 \ + --hash=sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b \ + --hash=sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9 \ + --hash=sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1 \ + --hash=sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746 + # via python-jsonrpc-server diff --git a/src/test/python_tests/test_data/sample1/sample.py b/src/test/python_tests/test_data/sample1/sample.py new file mode 100644 index 00000000..16ae5f39 --- /dev/null +++ b/src/test/python_tests/test_data/sample1/sample.py @@ -0,0 +1,3 @@ +import sys + +print(x) diff --git a/src/test/python_tests/test_data/sample1/sample.unformatted b/src/test/python_tests/test_data/sample1/sample.unformatted new file mode 100644 index 00000000..6c8771b9 --- /dev/null +++ b/src/test/python_tests/test_data/sample1/sample.unformatted @@ -0,0 +1 @@ +import sys;print(x) \ No newline at end of file diff --git a/src/test/python_tests/test_server.py b/src/test/python_tests/test_server.py new file mode 100644 index 00000000..fe4505c6 --- /dev/null +++ b/src/test/python_tests/test_server.py @@ -0,0 +1,148 @@ +# Copyright (c) ZenML GmbH 2024. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. +""" +Test for linting over LSP. +""" + +from threading import Event + +from hamcrest import assert_that, is_ + +from .lsp_test_client import constants, defaults, session, utils + +TEST_FILE_PATH = constants.TEST_DATA / "sample1" / "sample.py" +TEST_FILE_URI = utils.as_uri(str(TEST_FILE_PATH)) +SERVER_INFO = utils.get_server_info_defaults() +TIMEOUT = 10 # 10 seconds + + +def test_linting_example(): + """Test to linting on file open.""" + contents = TEST_FILE_PATH.read_text() + + actual = [] + with session.LspSession() as ls_session: + ls_session.initialize(defaults.VSCODE_DEFAULT_INITIALIZE) + + done = Event() + + def _handler(params): + nonlocal actual + actual = params + done.set() + + ls_session.set_notification_callback(session.PUBLISH_DIAGNOSTICS, _handler) + + ls_session.notify_did_open( + { + "textDocument": { + "uri": TEST_FILE_URI, + "languageId": "python", + "version": 1, + "text": contents, + } + } + ) + + # wait for some time to receive all notifications + done.wait(TIMEOUT) + + # TODO: Add your linter specific diagnostic result here + expected = { + "uri": TEST_FILE_URI, + "diagnostics": [ + { + # "range": { + # "start": {"line": 0, "character": 0}, + # "end": {"line": 0, "character": 0}, + # }, + # "message": "Missing module docstring", + # "severity": 3, + # "code": "C0114:missing-module-docstring", + "source": SERVER_INFO["name"], + }, + { + # "range": { + # "start": {"line": 2, "character": 6}, + # "end": { + # "line": 2, + # "character": 7, + # }, + # }, + # "message": "Undefined variable 'x'", + # "severity": 1, + # "code": "E0602:undefined-variable", + "source": SERVER_INFO["name"], + }, + { + # "range": { + # "start": {"line": 0, "character": 0}, + # "end": { + # "line": 0, + # "character": 10, + # }, + # }, + # "message": "Unused import sys", + # "severity": 2, + # "code": "W0611:unused-import", + "source": SERVER_INFO["name"], + }, + ], + } + + assert_that(actual, is_(expected)) + + +def test_formatting_example(): + """Test formatting a python file.""" + FORMATTED_TEST_FILE_PATH = constants.TEST_DATA / "sample1" / "sample.py" + UNFORMATTED_TEST_FILE_PATH = constants.TEST_DATA / "sample1" / "sample.unformatted" + + contents = UNFORMATTED_TEST_FILE_PATH.read_text() + lines = contents.splitlines(keepends=False) + + actual = [] + with utils.PythonFile(contents, UNFORMATTED_TEST_FILE_PATH.parent) as pf: + uri = utils.as_uri(str(pf.fullpath)) + + with session.LspSession() as ls_session: + ls_session.initialize() + ls_session.notify_did_open( + { + "textDocument": { + "uri": uri, + "languageId": "python", + "version": 1, + "text": contents, + } + } + ) + actual = ls_session.text_document_formatting( + { + "textDocument": {"uri": uri}, + # `options` is not used by black + "options": {"tabSize": 4, "insertSpaces": True}, + } + ) + + expected = [ + { + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": len(lines), "character": 0}, + }, + "newText": FORMATTED_TEST_FILE_PATH.read_text(), + } + ] + + assert_that(actual, is_(expected)) diff --git a/src/test/ts_tests/__mocks__/MockEventBus.ts b/src/test/ts_tests/__mocks__/MockEventBus.ts new file mode 100644 index 00000000..d32515ea --- /dev/null +++ b/src/test/ts_tests/__mocks__/MockEventBus.ts @@ -0,0 +1,55 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +import { EventEmitter } from 'stream'; + +export class MockEventBus extends EventEmitter { + public lsClientReady: boolean = false; + private static instance: MockEventBus; + public zenmlReady: boolean = false; + + constructor() { + super(); + this.on('lsClientReady', (isReady: boolean) => { + this.lsClientReady = isReady; + }); + } + + /** + * Retrieves the singleton instance of EventBus. + * + * @returns {MockEventBus} The singleton instance. + */ + public static getInstance(): MockEventBus { + if (!MockEventBus.instance) { + MockEventBus.instance = new MockEventBus(); + } + return MockEventBus.instance; + } + + /** + * Clears all event handlers. + */ + public clearAllHandlers() { + this.removeAllListeners(); + } + + /** + * Simulates setting the LS Client readiness state. + * + * @param isReady A boolean indicating whether the LS Client is ready. + * @returns void + */ + public setLsClientReady(isReady: boolean): void { + this.lsClientReady = isReady; + this.emit('lsClientReady', isReady); + } +} diff --git a/src/test/ts_tests/__mocks__/MockLSClient.ts b/src/test/ts_tests/__mocks__/MockLSClient.ts new file mode 100644 index 00000000..affddf62 --- /dev/null +++ b/src/test/ts_tests/__mocks__/MockLSClient.ts @@ -0,0 +1,151 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +import { MockEventBus } from './MockEventBus'; +import { MOCK_ACCESS_TOKEN, MOCK_REST_SERVER_DETAILS, MOCK_REST_SERVER_URL } from './constants'; + +interface MockLanguageClient { + start: () => Promise; + onNotification: (type: string, handler: (params: any) => void) => void; + sendRequest: (command: string, args?: any) => Promise; +} + +export class MockLSClient { + notificationHandlers: Map void> = new Map(); + mockLanguageClient: MockLanguageClient; + eventBus: MockEventBus; + private static instance: MockLSClient; + public clientReady: boolean = true; + + constructor(eventBus: MockEventBus) { + this.eventBus = eventBus; + this.mockLanguageClient = { + start: async () => {}, + onNotification: (type: string, handler: (params: any) => void) => { + this.notificationHandlers.set(type, handler); + }, + sendRequest: async (command: string, args?: any) => { + if (command === 'workspace/executeCommand') { + return this.sendLsClientRequest(args); + } else { + throw new Error(`Unmocked command: ${command}`); + } + }, + }; + } + + /** + * Retrieves the singleton instance of EventBus. + * + * @returns {MockLSClient} The singleton instance. + */ + public static getInstance(mockEventBus: MockEventBus): MockLSClient { + if (!MockLSClient.instance) { + MockLSClient.instance = new MockLSClient(mockEventBus); + } + return MockLSClient.instance; + } + + /** + * Starts the language client. + */ + public startLanguageClient(): Promise { + return this.mockLanguageClient.start(); + } + + /** + * Gets the mocked language client. + * + * @returns {MockLanguageClient} The mocked language client. + */ + public getLanguageClient(): MockLanguageClient { + return this.mockLanguageClient; + } + + /** + * Mocks sending a request to the language server. + * + * @param command The command to send to the language server. + * @param args The arguments to send with the command. + * @returns A promise resolving to a mocked response from the language server. + */ + async sendLsClientRequest(command: string, args: any[] = []): Promise { + switch (command) { + case 'connect': + if (args[0] === MOCK_REST_SERVER_URL) { + return Promise.resolve({ + message: 'Connected successfully', + access_token: MOCK_ACCESS_TOKEN, + }); + } else { + return Promise.reject(new Error('Failed to connect with incorrect URL')); + } + case 'disconnect': + return Promise.resolve({ message: 'Disconnected successfully' }); + case `serverInfo`: + return Promise.resolve(MOCK_REST_SERVER_DETAILS); + + case `renameStack`: + const [renameStackId, newStackName] = args; + if (renameStackId && newStackName) { + return Promise.resolve({ + message: `Stack ${renameStackId} successfully renamed to ${newStackName}.`, + }); + } else { + return Promise.resolve({ error: 'Failed to rename stack' }); + } + + case `copyStack`: + const [copyStackId, copyNewStackName] = args; + if (copyStackId && copyNewStackName) { + return Promise.resolve({ + message: `Stack ${copyStackId} successfully copied to ${copyNewStackName}.`, + }); + } else { + return Promise.resolve({ error: 'Failed to copy stack' }); + } + + case `switchActiveStack`: + const [stackNameOrId] = args; + if (stackNameOrId) { + return Promise.resolve({ message: `Active stack set to: ${stackNameOrId}` }); + } else { + return Promise.resolve({ error: 'Failed to set active stack' }); + } + + default: + return Promise.reject(new Error(`Unmocked command: ${command}`)); + } + } + + /** + * Triggers a notification with the given type and parameters. + * + * @param type The type of the notification. + * @param params The parameters of the notification. + * @returns void + */ + public triggerNotification(type: string, params: any): void { + const handler = this.notificationHandlers.get(type); + if (handler) { + handler(params); + if (type === 'zenml/serverChanged') { + this.eventBus.emit('zenml/serverChanged', { + updatedServerConfig: params, + }); + } else if (type === 'zenml/requirementNotMet') { + this.eventBus.emit('lsClientReady', false); + } else if (type === 'zenml/ready') { + this.eventBus.emit('lsClientReady', true); + } + } + } +} diff --git a/src/test/ts_tests/__mocks__/MockViewProviders.ts b/src/test/ts_tests/__mocks__/MockViewProviders.ts new file mode 100644 index 00000000..08cf1333 --- /dev/null +++ b/src/test/ts_tests/__mocks__/MockViewProviders.ts @@ -0,0 +1,48 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { StackDataProvider } from '../../../views/activityBar'; +import { ServerDataProvider } from '../../../views/activityBar'; +import { ServerStatus, ZenServerDetails } from '../../../types/ServerInfoTypes'; +import { INITIAL_ZENML_SERVER_STATUS } from '../../../utils/constants'; +import ZenMLStatusBar from '../../../views/statusBar'; +import sinon from 'sinon'; + +export class MockZenMLStatusBar extends ZenMLStatusBar { + public refreshActiveStack = sinon.stub().resolves(); +} + +export class MockStackDataProvider extends StackDataProvider { + public refresh = sinon.stub().resolves(); +} + +export class MockServerDataProvider extends ServerDataProvider { + public refreshCalled: boolean = false; + public currentServerStatus: ServerStatus = INITIAL_ZENML_SERVER_STATUS; + + public async refresh(updatedServerConfig?: ZenServerDetails): Promise { + this.refreshCalled = true; + if (updatedServerConfig) { + this.currentServerStatus = { + ...updatedServerConfig.storeInfo, + isConnected: updatedServerConfig.storeConfig.type === 'rest', + url: updatedServerConfig.storeConfig.url, + store_type: updatedServerConfig.storeConfig.type, + }; + } + } + + public resetMock(): void { + this.refreshCalled = false; + this.currentServerStatus = INITIAL_ZENML_SERVER_STATUS; + } +} diff --git a/src/test/ts_tests/__mocks__/constants.ts b/src/test/ts_tests/__mocks__/constants.ts new file mode 100644 index 00000000..a549fe5e --- /dev/null +++ b/src/test/ts_tests/__mocks__/constants.ts @@ -0,0 +1,121 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import { ServerStatus, ZenServerDetails } from '../../../types/ServerInfoTypes'; + +export const MOCK_REST_SERVER_URL = 'https://zenml.example.com'; +export const MOCK_SQL_SERVER_URL = 'sqlite:///path/to/sqlite.db'; +export const MOCK_SERVER_ID = 'test-server'; +export const MOCK_AUTH_SCHEME = 'OAUTH2_PASSWORD_BEARER'; +export const MOCK_ZENML_VERSION = '0.55.5'; +export const MOCK_ACCESS_TOKEN = 'valid_token'; + +export const MOCK_CONTEXT = { + subscriptions: [], + extensionUri: vscode.Uri.parse('file:///extension/path'), + storagePath: '/path/to/storage', + globalStoragePath: '/path/to/global/storage', + workspaceState: { get: sinon.stub(), update: sinon.stub() }, + globalState: { get: sinon.stub(), update: sinon.stub(), setKeysForSync: sinon.stub() }, + logPath: '/path/to/log', + asAbsolutePath: sinon.stub(), +} as any; + +export const MOCK_REST_SERVER_STATUS: ServerStatus = { + isConnected: true, + id: MOCK_SERVER_ID, + store_type: 'rest', + url: MOCK_REST_SERVER_URL, + version: MOCK_ZENML_VERSION, + debug: false, + deployment_type: 'kubernetes', + database_type: 'sqlite', + secrets_store_type: 'sql', + auth_scheme: MOCK_AUTH_SCHEME, + dashboard_url: '', +}; + +export const MOCK_REST_SERVER_DETAILS: ZenServerDetails = { + storeInfo: { + id: MOCK_SERVER_ID, + version: MOCK_ZENML_VERSION, + debug: false, + deployment_type: 'kubernetes', + database_type: 'sqlite', + secrets_store_type: 'sql', + auth_scheme: MOCK_AUTH_SCHEME, + dashboard_url: '', + }, + storeConfig: { + type: 'rest', + url: MOCK_REST_SERVER_URL, + secrets_store: null, + backup_secrets_store: null, + username: null, + password: null, + api_key: 'api_key', + verify_ssl: true, + pool_pre_ping: true, + http_timeout: 30, + }, +}; + +export const MOCK_SQL_SERVER_STATUS: ServerStatus = { + isConnected: false, + id: MOCK_SERVER_ID, + store_type: 'sql', + url: MOCK_SQL_SERVER_URL, + version: MOCK_ZENML_VERSION, + debug: false, + deployment_type: 'local', + database_type: 'sqlite', + secrets_store_type: 'sql', + auth_scheme: MOCK_AUTH_SCHEME, + dashboard_url: '', +}; + +export const MOCK_SQL_SERVER_DETAILS: ZenServerDetails = { + storeInfo: { + id: MOCK_SERVER_ID, + version: MOCK_ZENML_VERSION, + debug: false, + deployment_type: 'local', + database_type: 'sqlite', + secrets_store_type: 'sql', + auth_scheme: MOCK_AUTH_SCHEME, + dashboard_url: '', + }, + storeConfig: { + type: 'sql', + url: MOCK_SQL_SERVER_URL, + secrets_store: null, + backup_secrets_store: null, + username: null, + password: null, + verify_ssl: false, + pool_pre_ping: true, + http_timeout: 30, + driver: '', + database: '', + ssl_ca: '', + ssl_key: '', + ssl_verify_server_cert: false, + ssl_cert: '', + pool_size: 0, + max_overflow: 0, + backup_strategy: '', + backup_directory: '', + backup_database: '', + }, +}; diff --git a/src/test/ts_tests/commands/serverCommands.test.ts b/src/test/ts_tests/commands/serverCommands.test.ts new file mode 100644 index 00000000..2079530c --- /dev/null +++ b/src/test/ts_tests/commands/serverCommands.test.ts @@ -0,0 +1,130 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import { serverCommands } from '../../../commands/server/cmds'; +import { EventBus } from '../../../services/EventBus'; +import { LSClient } from '../../../services/LSClient'; +import { refreshUtils } from '../../../utils/refresh'; +import { ServerDataProvider } from '../../../views/activityBar'; +import { MockEventBus } from '../__mocks__/MockEventBus'; +import { MockLSClient } from '../__mocks__/MockLSClient'; +import { MOCK_ACCESS_TOKEN, MOCK_REST_SERVER_URL } from '../__mocks__/constants'; + +suite('Server Commands Tests', () => { + let sandbox: sinon.SinonSandbox; + let showErrorMessageStub: sinon.SinonStub; + let mockLSClient: any; + let mockEventBus = new MockEventBus(); + let emitSpy: sinon.SinonSpy; + let configurationMock: any; + let showInputBoxStub: sinon.SinonStub; + let refreshUIComponentsStub: sinon.SinonStub; + + setup(() => { + sandbox = sinon.createSandbox(); + mockLSClient = new MockLSClient(mockEventBus); + emitSpy = sandbox.spy(mockEventBus, 'emit'); + sandbox.stub(LSClient, 'getInstance').returns(mockLSClient); + sandbox.stub(EventBus, 'getInstance').returns(mockEventBus); + showInputBoxStub = sandbox.stub(vscode.window, 'showInputBox'); + showErrorMessageStub = sandbox.stub(vscode.window, 'showErrorMessage'); + + configurationMock = { + get: sandbox.stub().withArgs('serverUrl').returns(MOCK_REST_SERVER_URL), + update: sandbox.stub().resolves(), + has: sandbox.stub().returns(false), + inspect: sandbox.stub().returns({ globalValue: undefined }), + }; + sandbox.stub(vscode.workspace, 'getConfiguration').returns(configurationMock); + sandbox.stub(vscode.window, 'withProgress').callsFake(async (options, task) => { + const mockProgress = { + report: sandbox.stub(), + }; + const mockCancellationToken = new vscode.CancellationTokenSource(); + await task(mockProgress, mockCancellationToken.token); + }); + + refreshUIComponentsStub = sandbox + .stub(refreshUtils, 'refreshUIComponents') + .callsFake(async () => { + console.log('Stubbed refreshUIComponents called'); + }); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('connectServer successfully connects to the server', async () => { + showInputBoxStub.resolves(MOCK_REST_SERVER_URL); + sandbox + .stub(mockLSClient, 'sendLsClientRequest') + .withArgs('connect', [MOCK_REST_SERVER_URL]) + .resolves({ + message: 'Connected successfully', + access_token: MOCK_ACCESS_TOKEN, + }); + + const result = await serverCommands.connectServer(); + + assert.strictEqual(result, true, 'Should successfully connect to the server'); + sinon.assert.calledOnce(showInputBoxStub); + sinon.assert.calledWith( + configurationMock.update, + 'serverUrl', + MOCK_REST_SERVER_URL, + vscode.ConfigurationTarget.Global + ); + sinon.assert.calledWith( + configurationMock.update, + 'accessToken', + MOCK_ACCESS_TOKEN, + vscode.ConfigurationTarget.Global + ); + }); + + test('disconnectServer successfully disconnects from the server', async () => { + sandbox + .stub(mockLSClient, 'sendLsClientRequest') + .withArgs('disconnect') + .resolves({ message: 'Disconnected successfully' }); + + await serverCommands.disconnectServer(); + + sinon.assert.calledOnce(refreshUIComponentsStub); + }); + + test('connectServer fails with incorrect URL', async () => { + showInputBoxStub.resolves('invalid.url'); + sandbox + .stub(mockLSClient, 'sendLsClientRequest') + .withArgs('connect', ['invalid.url']) + .rejects(new Error('Failed to connect')); + + const result = await serverCommands.connectServer(); + assert.strictEqual(result, false, 'Should fail to connect to the server with incorrect URL'); + sinon.assert.calledOnce(showErrorMessageStub); + }); + + test('refreshServerStatus refreshes the server status', async () => { + const serverDataProviderRefreshStub = sandbox + .stub(ServerDataProvider.getInstance(), 'refresh') + .resolves(); + + await serverCommands.refreshServerStatus(); + + sinon.assert.calledOnce(serverDataProviderRefreshStub); + }); +}); diff --git a/src/test/ts_tests/commands/stackCommands.test.ts b/src/test/ts_tests/commands/stackCommands.test.ts new file mode 100644 index 00000000..0bda457f --- /dev/null +++ b/src/test/ts_tests/commands/stackCommands.test.ts @@ -0,0 +1,159 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as assert from 'assert'; +import sinon from 'sinon'; +import * as vscode from 'vscode'; +import { stackCommands } from '../../../commands/stack/cmds'; +import ZenMLStatusBar from '../../../views/statusBar'; +import { StackDataProvider, StackTreeItem } from '../../../views/activityBar'; +import { MockEventBus } from '../__mocks__/MockEventBus'; +import { MockLSClient } from '../__mocks__/MockLSClient'; +import { LSClient } from '../../../services/LSClient'; +import { EventBus } from '../../../services/EventBus'; +import * as globalUtils from '../../../utils/global'; +import stackUtils from '../../../commands/stack/utils'; +import { MockStackDataProvider, MockZenMLStatusBar } from '../__mocks__/MockViewProviders'; + +suite('Stack Commands Test Suite', () => { + let sandbox: sinon.SinonSandbox; + let showInputBoxStub: sinon.SinonStub; + let showInformationMessageStub: sinon.SinonStub; + let mockLSClient: any; + let mockEventBus: any; + let mockStackDataProvider: MockStackDataProvider; + let mockStatusBar: MockZenMLStatusBar; + let switchActiveStackStub: sinon.SinonStub; + let setActiveStackStub: sinon.SinonStub; + + setup(() => { + sandbox = sinon.createSandbox(); + mockEventBus = new MockEventBus(); + mockLSClient = new MockLSClient(mockEventBus); + mockStackDataProvider = new MockStackDataProvider(); + mockStatusBar = new MockZenMLStatusBar(); + const stubbedServerUrl = 'http://mocked-server.com'; + + // Stub classes to return mock instances + sandbox.stub(StackDataProvider, 'getInstance').returns(mockStackDataProvider); + sandbox.stub(ZenMLStatusBar, 'getInstance').returns(mockStatusBar); + sandbox.stub(LSClient, 'getInstance').returns(mockLSClient); + sandbox.stub(EventBus, 'getInstance').returns(mockEventBus); + sandbox.stub(stackUtils, 'storeActiveStack').resolves(); + sandbox.stub(globalUtils, 'getZenMLServerUrl').returns(stubbedServerUrl); + + showInputBoxStub = sandbox.stub(vscode.window, 'showInputBox'); + showInformationMessageStub = sandbox.stub(vscode.window, 'showInformationMessage'); + + switchActiveStackStub = sandbox + .stub(stackUtils, 'switchActiveStack') + .callsFake(async (stackNameOrId: string) => { + console.log('switchActiveStack stub called with', stackNameOrId); + return Promise.resolve({ id: stackNameOrId, name: `MockStackName` }); + }); + + setActiveStackStub = sandbox + .stub(stackCommands, 'setActiveStack') + .callsFake(async (node: StackTreeItem) => { + await switchActiveStackStub(node.id); + showInformationMessageStub(`Active stack set to: ${node.label}`); + await mockStatusBar.refreshActiveStack(); + await mockStackDataProvider.refresh(); + }); + + sandbox.stub(vscode.window, 'withProgress').callsFake(async (options, task) => { + const mockProgress = { + report: sandbox.stub(), + }; + const mockCancellationToken = new vscode.CancellationTokenSource(); + await task(mockProgress, mockCancellationToken.token); + }); + }); + + teardown(() => { + sandbox.restore(); + mockEventBus.clearAllHandlers(); + }); + + test('renameStack successfully renames a stack', async () => { + const stackId = 'stack-id-123'; + const newStackName = 'New Stack Name'; + + showInputBoxStub.resolves(newStackName); + await stackCommands.renameStack({ label: 'Old Stack', id: stackId } as any); + + assert.strictEqual(showInputBoxStub.calledOnce, true); + }); + + test('copyStack successfully copies a stack', async () => { + const sourceStackId = 'stack-id-789'; + const targetStackName = 'Copied Stack'; + + showInputBoxStub.resolves(targetStackName); + await stackCommands.copyStack({ label: 'Source Stack', id: sourceStackId } as any); + + sinon.assert.calledOnce(showInputBoxStub); + assert.strictEqual( + showInputBoxStub.calledWithExactly({ prompt: 'Enter the name for the copied stack' }), + true, + 'Input box was not called with the correct prompt' + ); + }); + + test('goToStackUrl opens the correct URL and shows an information message', () => { + const stackId = 'stack-id-123'; + const expectedUrl = stackUtils.getStackDashboardUrl(stackId); + + const openExternalStub = sandbox.stub(vscode.env, 'openExternal'); + + stackCommands.goToStackUrl({ label: 'Stack', id: stackId } as any); + + assert.strictEqual(openExternalStub.calledOnce, true, 'openExternal should be called once'); + assert.strictEqual( + openExternalStub.args[0][0].toString(), + expectedUrl, + 'Correct URL should be passed to openExternal' + ); + assert.strictEqual( + showInformationMessageStub.calledOnce, + true, + 'showInformationMessage should be called once' + ); + assert.strictEqual( + showInformationMessageStub.args[0][0], + `Opening: ${expectedUrl}`, + 'Correct information message should be shown' + ); + }); + + test('stackDataProviderMock.refresh can be called directly', async () => { + await mockStackDataProvider.refresh(); + sinon.assert.calledOnce(mockStackDataProvider.refresh); + }); + + test('refreshActiveStack successfully refreshes the active stack', async () => { + await stackCommands.refreshActiveStack(); + sinon.assert.calledOnce(mockStatusBar.refreshActiveStack); + }); + + test('setActiveStack successfully switches to a new stack', async () => { + const fakeStackNode = new StackTreeItem('MockStackName', 'fake-stack-id', [], false); + + await stackCommands.setActiveStack(fakeStackNode); + + sinon.assert.calledOnce(switchActiveStackStub); + sinon.assert.calledOnce(showInformationMessageStub); + sinon.assert.calledWith(showInformationMessageStub, `Active stack set to: MockStackName`); + sinon.assert.calledOnce(mockStackDataProvider.refresh); + sinon.assert.calledOnce(mockStatusBar.refreshActiveStack); + }); +}); diff --git a/src/test/ts_tests/extension.test.ts b/src/test/ts_tests/extension.test.ts new file mode 100644 index 00000000..55b6582a --- /dev/null +++ b/src/test/ts_tests/extension.test.ts @@ -0,0 +1,54 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as assert from 'assert'; +import sinon from 'sinon'; +import * as vscode from 'vscode'; +import * as extension from '../../extension'; +import { EventBus } from '../../services/EventBus'; +import { LSClient } from '../../services/LSClient'; +import { ZenExtension } from '../../services/ZenExtension'; +import { MockEventBus } from './__mocks__/MockEventBus'; + +suite('Extension Activation Test Suite', () => { + let sandbox: sinon.SinonSandbox; + let contextMock: any; + let initializeSpy: sinon.SinonSpy; + let mockEventBus = new MockEventBus(); + let lsClient: LSClient; + + setup(() => { + sandbox = sinon.createSandbox(); + contextMock = { + subscriptions: [], + extensionPath: '', + extensionUri: vscode.Uri.file('/'), + }; + initializeSpy = sinon.spy(ZenExtension, 'activate'); + lsClient = LSClient.getInstance(); + sandbox.stub(EventBus, 'getInstance').returns(mockEventBus); + }); + + teardown(() => { + sandbox.restore(); + initializeSpy.restore(); + }); + + test('ZenML Extension should be present', () => { + assert.ok(vscode.extensions.getExtension('ZenML.zenml-vscode')); + }); + + test('activate function behaves as expected', async () => { + await extension.activate(contextMock); + sinon.assert.calledOnceWithExactly(initializeSpy, contextMock, lsClient); + }); +}); diff --git a/src/test/ts_tests/integration/serverConfigUpdate.test.ts b/src/test/ts_tests/integration/serverConfigUpdate.test.ts new file mode 100644 index 00000000..913f1d65 --- /dev/null +++ b/src/test/ts_tests/integration/serverConfigUpdate.test.ts @@ -0,0 +1,85 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import assert from 'assert'; +import { EventBus } from '../../../services/EventBus'; +import { ZenServerDetails } from '../../../types/ServerInfoTypes'; +import { MOCK_REST_SERVER_DETAILS } from '../__mocks__/constants'; +import { MockLSClient } from '../__mocks__/MockLSClient'; +import { MockEventBus } from '../__mocks__/MockEventBus'; +import { LSCLIENT_READY, LSP_ZENML_SERVER_CHANGED } from '../../../utils/constants'; + +suite('Server Configuration Update Flow Tests', () => { + let sandbox: sinon.SinonSandbox; + let mockEventBus = new MockEventBus(); + let mockLSClientInstance: MockLSClient; + let mockLSClient: any; + let refreshUIComponentsStub: sinon.SinonStub; + + setup(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(vscode.window, 'showInformationMessage'); + refreshUIComponentsStub = sandbox.stub().resolves(); + + // Mock LSClient + mockLSClientInstance = new MockLSClient(mockEventBus); + mockLSClient = mockLSClientInstance.getLanguageClient(); + sandbox.stub(mockLSClientInstance, 'startLanguageClient').resolves(); + + // Mock EventBus + sandbox.stub(EventBus, 'getInstance').returns(mockEventBus); + mockEventBus.on(LSCLIENT_READY, async (isReady: boolean) => { + if (isReady) { + await refreshUIComponentsStub(); + } + }); + mockEventBus.on(LSP_ZENML_SERVER_CHANGED, async (updatedServerConfig: ZenServerDetails) => { + await refreshUIComponentsStub(updatedServerConfig); + }); + }); + + teardown(() => { + sandbox.restore(); + mockEventBus.clearAllHandlers(); + }); + + test('LSClientReady event triggers UI refresh', async () => { + mockEventBus.setLsClientReady(true); + sinon.assert.calledOnce(refreshUIComponentsStub); + }); + + test('MockLSClient triggerNotification works as expected', async () => { + const mockNotificationType = 'testNotification'; + const mockData = { key: 'value' }; + mockLSClientInstance.mockLanguageClient.onNotification(mockNotificationType, (data: any) => { + assert.deepStrictEqual(data, mockData); + }); + mockLSClientInstance.triggerNotification(mockNotificationType, mockData); + }); + + test('zenml/serverChanged event updates global configuration and refreshes UI', async () => { + mockLSClientInstance.mockLanguageClient.onNotification( + LSP_ZENML_SERVER_CHANGED, + (data: ZenServerDetails) => { + assert.deepStrictEqual(data, MOCK_REST_SERVER_DETAILS); + } + ); + + mockLSClientInstance.triggerNotification(LSP_ZENML_SERVER_CHANGED, MOCK_REST_SERVER_DETAILS); + + await new Promise(resolve => setTimeout(resolve, 0)); + + sinon.assert.calledOnce(refreshUIComponentsStub); + }); +}); diff --git a/src/test/ts_tests/unit/ServerDataProvider.test.ts b/src/test/ts_tests/unit/ServerDataProvider.test.ts new file mode 100644 index 00000000..101b0684 --- /dev/null +++ b/src/test/ts_tests/unit/ServerDataProvider.test.ts @@ -0,0 +1,95 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { serverUtils } from '../../../commands/server/utils'; +import { EventBus } from '../../../services/EventBus'; +import { LSClient } from '../../../services/LSClient'; +import { ServerDataProvider } from '../../../views/activityBar'; +import { MockLSClient } from '../__mocks__/MockLSClient'; +import { MOCK_REST_SERVER_STATUS, MOCK_SQL_SERVER_STATUS } from '../__mocks__/constants'; +import { MockEventBus } from '../__mocks__/MockEventBus'; +import { ServerStatus } from '../../../types/ServerInfoTypes'; +import { LOADING_TREE_ITEMS } from '../../../views/activityBar/common/LoadingTreeItem'; + +suite('ServerDataProvider Tests', () => { + let sandbox: sinon.SinonSandbox; + let mockEventBus: MockEventBus; + let serverDataProvider: ServerDataProvider; + let mockLSClientInstance: any; + let mockLSClient: any; + + setup(() => { + sandbox = sinon.createSandbox(); + serverDataProvider = ServerDataProvider.getInstance(); + mockEventBus = new MockEventBus(); + mockLSClientInstance = MockLSClient.getInstance(mockEventBus); + mockLSClient = mockLSClientInstance.getLanguageClient(); + sandbox.stub(LSClient, 'getInstance').returns(mockLSClientInstance); + sandbox.stub(EventBus, 'getInstance').returns(mockEventBus); + sandbox.stub(mockLSClientInstance, 'startLanguageClient').resolves(); + serverDataProvider['zenmlClientReady'] = true; + }); + + teardown(() => { + sandbox.restore(); + }); + + test('ServerDataProvider initializes correctly', async () => { + assert.ok(serverDataProvider); + }); + + test('ServerDataProvider should update server status correctly', async () => { + sandbox.stub(serverUtils, 'checkServerStatus').callsFake(async () => { + return Promise.resolve(MOCK_REST_SERVER_STATUS); + }); + + await serverDataProvider.refresh(); + const serverStatus = serverDataProvider.getCurrentStatus() as ServerStatus; + + assert.strictEqual( + serverStatus.isConnected, + true, + 'Server should be reported as connected for REST config' + ); + }); + + test('ServerDataProvider should update server status to disconnected for non-REST type', async () => { + sandbox.restore(); + + sandbox.stub(serverUtils, 'checkServerStatus').callsFake(async () => { + return Promise.resolve(MOCK_SQL_SERVER_STATUS); + }); + + await serverDataProvider.refresh(); + const serverStatus = serverDataProvider.getCurrentStatus() as ServerStatus; + + assert.strictEqual( + serverStatus.isConnected, + false, + 'Server should be reported as disconnected for SQL config' + ); + }); + test('ServerDataProvider should handle zenmlClient not ready state', async () => { + serverDataProvider['zenmlClientReady'] = false; + + await serverDataProvider.refresh(); + assert.deepStrictEqual( + serverDataProvider.getCurrentStatus(), + [LOADING_TREE_ITEMS.get('zenmlClient')!], + 'ServerDataProvider should show loading state for ZenML client not ready' + ); + + serverDataProvider['zenmlClientReady'] = true; + }); +}); diff --git a/src/test/ts_tests/unit/eventBus.test.ts b/src/test/ts_tests/unit/eventBus.test.ts new file mode 100644 index 00000000..deb70edc --- /dev/null +++ b/src/test/ts_tests/unit/eventBus.test.ts @@ -0,0 +1,50 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import assert from 'assert'; +import sinon from 'sinon'; +import { MockEventBus } from '../__mocks__/MockEventBus'; +import { LSCLIENT_READY } from '../../../utils/constants'; + +suite('MockEventBus and Event Handling', () => { + let eventBus: MockEventBus; + let spy: sinon.SinonSpy; + + setup(() => { + eventBus = new MockEventBus(); + spy = sinon.spy(); + }); + + test('handles lsClientReady event correctly with mock', () => { + eventBus.on(LSCLIENT_READY, spy); + eventBus.emit(LSCLIENT_READY, true); + assert.ok( + spy.calledWith(true), + 'lsClientReady event handler was not called with expected argument' + ); + }); + + test('can clear all event handlers and not trigger events', () => { + eventBus.on(LSCLIENT_READY, spy); + eventBus.clearAllHandlers(); + + // Try emitting the event after clearing all handlers + eventBus.emit(LSCLIENT_READY, true); + + // Verify the spy was not called since all handlers were cleared + assert.strictEqual( + spy.called, + false, + 'lsClientReady event handler was called despite clearing all handlers' + ); + }); +}); diff --git a/src/types/HydratedTypes.ts b/src/types/HydratedTypes.ts new file mode 100644 index 00000000..525a0577 --- /dev/null +++ b/src/types/HydratedTypes.ts @@ -0,0 +1,141 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. + +/************************************************************************************************ + * Hydrated User types from the ZenML Client. + ************************************************************************************************/ +interface User { + body: UserBody; + metadata?: UserMetadata; + resources?: null; + id: string; + permission_denied: boolean; + name: string; +} + +interface UserMetadata { + email?: string | null; + hub_token?: string | null; + external_user_id?: string | null; +} + +interface UserBody { + created: string; + updated: string; + active: boolean; + activation_token?: null; + full_name?: string; + email_opted_in?: null; + is_service_account: boolean; +} + +/************************************************************************************************ + * Hydrated Workspace types from the ZenML Client. + ************************************************************************************************/ +interface Workspace { + body: WorkspaceBody; + metadata?: { + description?: string; + }; + resources?: any | null; + id: string; + permission_denied: boolean; + name: string; +} + +interface WorkspaceBody { + created: string; + updated: string; +} + +interface Workspace { + body: WorkspaceBody; + metadata?: { + description?: string; + }; + resources?: any | null; + id: string; + permission_denied: boolean; + name: string; +} + +interface WorkspaceBody { + created: string; + updated: string; +} + +/************************************************************************************************ + * Hydrated Stack / Components types from the ZenML Client. + ************************************************************************************************/ +interface HydratedStack { + id: string; + name: string; + permission_denied: boolean; + body: { + created: string; + updated: string; + user?: User | null; + }; + metadata: StackMetadata; + resources?: null; +} + +interface HydratedStackComponent { + body: StackComponentBody; + metadata: ComponentMetadata; + resources?: null; + id: string; + permission_denied: boolean; + name: string; +} + +interface StackComponentBody { + created: string; + updated: string; + user?: User | null; + type: string; + flavor: string; +} + +interface ComponentMetadata { + workspace: Workspace; + configuration?: any; + labels?: null; + component_spec_path?: null; + connector_resource_id?: null; + connector?: null; +} + +interface StackMetadata { + workspace: Workspace; + components: HydratedComponents; + description: string; + stack_spec_path?: null; +} + +interface HydratedComponents { + orchestrator?: HydratedStackComponent[]; + artifact_store?: HydratedStackComponent[]; + container_registry?: HydratedStackComponent[]; + model_registry?: HydratedStackComponent[]; + step_operator?: HydratedStackComponent[]; + feature_store?: HydratedStackComponent[]; + model_deployer?: HydratedStackComponent[]; + experiment_tracker?: HydratedStackComponent[]; + alerter?: HydratedStackComponent[]; + annotator?: HydratedStackComponent[]; + data_validator?: HydratedStackComponent[]; + image_builder?: HydratedStackComponent[]; +} + +export { Workspace, WorkspaceBody, User, UserBody, UserMetadata }; diff --git a/src/types/LSClientResponseTypes.ts b/src/types/LSClientResponseTypes.ts new file mode 100644 index 00000000..f86f1f80 --- /dev/null +++ b/src/types/LSClientResponseTypes.ts @@ -0,0 +1,54 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. + +import { ZenServerDetails } from './ServerInfoTypes'; + +/***** Generic Response Types *****/ +export interface SuccessMessageResponse { + message: string; +} + +export interface ErrorMessageResponse { + error: string; + message: string; +} + +export interface VersionMismatchError { + error: string; + message: string; + clientVersion: string; + serverVersion: string; +} + +export type GenericLSClientResponse = SuccessMessageResponse | ErrorMessageResponse; + +/***** Server Response Types *****/ +export interface RestServerConnectionResponse { + message: string; + access_token: string; +} + +export type ServerStatusInfoResponse = + | ZenServerDetails + | VersionMismatchError + | ErrorMessageResponse; +export type ConnectServerResponse = RestServerConnectionResponse | ErrorMessageResponse; + +/***** Stack Response Types *****/ +export interface ActiveStackResponse { + id: string; + name: string; +} + +export type SetActiveStackResponse = ActiveStackResponse | ErrorMessageResponse; +export type GetActiveStackResponse = ActiveStackResponse | ErrorMessageResponse; diff --git a/src/types/LSNotificationTypes.ts b/src/types/LSNotificationTypes.ts new file mode 100644 index 00000000..944782a4 --- /dev/null +++ b/src/types/LSNotificationTypes.ts @@ -0,0 +1,23 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. + +// create type for LSP server notification that returns {is_installed: boolean, version: string} + +export interface LSNotificationIsZenMLInstalled { + is_installed: boolean; + version?: string; +} + +export interface LSNotification { + is_ready: boolean; +} diff --git a/src/types/PipelineTypes.ts b/src/types/PipelineTypes.ts new file mode 100644 index 00000000..19315ce1 --- /dev/null +++ b/src/types/PipelineTypes.ts @@ -0,0 +1,71 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. + +import { ErrorMessageResponse, VersionMismatchError } from './LSClientResponseTypes'; + +interface PipelineRunsData { + runs: PipelineRun[]; + total: number; + total_pages: number; + current_page: number; + items_per_page: number; +} + +export interface PipelineRun { + id: string; + name: string; + status: string; + stackName: string; + startTime: string; + endTime: string; + os: string; + osVersion: string; + pythonVersion: string; +} + +export interface DagStep { + id: string; + type: 'step'; + data: { + execution_id: string; + name: string; + status: 'initializing' | 'failed' | 'completed' | 'running' | 'cached'; + }; +} + +export interface DagArtifact { + id: string; + type: 'artifact'; + data: { + execution_id: string; + name: string; + artifact_type: string; + }; +} + +export type DagNode = DagStep | DagArtifact; + +export interface DagEdge { + id: string; + source: string; + target: string; +} + +export interface PipelineRunDag { + nodes: Array; + edges: Array; + status: string; + name: string; +} + +export type PipelineRunsResponse = PipelineRunsData | ErrorMessageResponse | VersionMismatchError; diff --git a/src/types/ServerInfoTypes.ts b/src/types/ServerInfoTypes.ts new file mode 100644 index 00000000..8006e575 --- /dev/null +++ b/src/types/ServerInfoTypes.ts @@ -0,0 +1,104 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +export interface ServerStatus { + isConnected: boolean; + url: string; + dashboard_url: string; + version: string; + store_type: string; + deployment_type: string; + database_type: string; + secrets_store_type: string; + database?: string; + backup_directory?: string; + backup_strategy?: string; + auth_scheme?: string; + debug?: boolean; + id?: string; + username?: string | null; +} + +/************************************************************************************************ + * This is the object returned by the @LSP_SERVER.command(zenml.serverInfo") + ************************************************************************************************/ +export interface ZenServerDetails { + storeInfo: ZenServerStoreInfo; + storeConfig: ZenServerStoreConfig; +} + +export interface ConfigUpdateDetails { + url: string; + api_token: string; + store_type: string; +} + +/************************************************************************************************ + * This is the response from the `zen_store.get_store_info()` method in the ZenML Client. + ************************************************************************************************/ +export interface ZenServerStoreInfo { + id: string; + version: string; + debug: boolean; + deployment_type: string; + database_type: string; + secrets_store_type: string; + auth_scheme: string; + base_url?: string; + metadata?: any; + dashboard_url: string; +} + +/************************************************************************************************ + * This is the response from the `zen_store.get_store_config()` method in the ZenML Client. + ************************************************************************************************/ +export type ZenServerStoreConfig = RestZenServerStoreConfig | SQLZenServerStoreConfig; + +/************************************************************************************************ + * REST Zen Server Store Config (type === 'rest') + ************************************************************************************************/ +export interface RestZenServerStoreConfig { + type: string; + url: string; + secrets_store: any; + backup_secrets_store: any; + username: string | null; + password: string | null; + api_key: any; + api_token?: string; + verify_ssl: boolean; + http_timeout: number; +} + +/************************************************************************************************ + * SQL Zen Server Store Config (type === 'sql') + ************************************************************************************************/ +export interface SQLZenServerStoreConfig { + type: string; + url: string; + secrets_store: any; + backup_secrets_store: any; + driver: string; + database: string; + username: string | null; + password: string | null; + ssl_ca: string | null; + ssl_cert: string | null; + ssl_key: string | null; + ssl_verify_server_cert: boolean; + pool_size: number; + max_overflow: number; + pool_pre_ping: boolean; + backup_strategy: string; + backup_directory: string; + backup_database: string | null; +} diff --git a/src/types/StackTypes.ts b/src/types/StackTypes.ts new file mode 100644 index 00000000..00fbdcde --- /dev/null +++ b/src/types/StackTypes.ts @@ -0,0 +1,96 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. + +import { ErrorMessageResponse, VersionMismatchError } from './LSClientResponseTypes'; + +/************************************************************************************************ + * LSClient parses the JSON response from the ZenML Client, and returns the following types. + * Hydrated types are in the HydratedTypes.ts file. + ************************************************************************************************/ +interface StacksData { + stacks: Stack[]; + total: number; + total_pages: number; + current_page: number; + items_per_page: number; +} + +interface Stack { + id: string; + name: string; + components: Components; +} + +interface Components { + [componentType: string]: StackComponent[]; +} + +interface StackComponent { + id: string; + name: string; + flavor: string; + type: string; + config: { [key: string]: any }; +} + +export type StacksResponse = StacksData | ErrorMessageResponse | VersionMismatchError; + +interface ComponentsListData { + index: number; + max_size: number; + total_pages: number; + total: number; + items: Array; +} + +export type ComponentsListResponse = + | ComponentsListData + | ErrorMessageResponse + | VersionMismatchError; + +interface Flavor { + id: string; + name: string; + type: string; + logo_url: string; + config_schema: { [key: string]: any }; + docs_url: string | null; + sdk_docs_url: string | null; + connector_type: string | null; + connector_resource_type: string | null; + connector_resource_id_attr: string | null; +} + +interface FlavorListData { + index: number; + max_size: number; + total_pages: number; + total: number; + items: Flavor[]; +} + +export type FlavorListResponse = FlavorListData | ErrorMessageResponse | VersionMismatchError; + +type ComponentTypes = string[]; + +export type ComponentTypesResponse = ComponentTypes | VersionMismatchError | ErrorMessageResponse; + +export { + Stack, + Components, + StackComponent, + StacksData, + ComponentsListData, + Flavor, + ComponentTypes, +}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 00000000..ce438062 --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,61 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { ServerStatus } from '../types/ServerInfoTypes'; + +export const PYTOOL_MODULE = 'zenml-python'; +export const PYTOOL_DISPLAY_NAME = 'ZenML'; +export const LANGUAGE_SERVER_NAME = 'zen-language-server'; +export const MIN_ZENML_VERSION = '0.55.0'; +export const ZENML_EMOJI = '⛩️'; + +export const ZENML_PYPI_URL = 'https://pypi.org/pypi/zenml/json'; +export const DEFAULT_LOCAL_ZENML_SERVER_URL = 'http://127.0.0.1:8237'; + +// LSP server notifications +export const LSP_IS_ZENML_INSTALLED = 'zenml/isInstalled'; +export const LSP_ZENML_CLIENT_INITIALIZED = 'zenml/clientInitialized'; +export const LSP_ZENML_SERVER_CHANGED = 'zenml/serverChanged'; +export const LSP_ZENML_STACK_CHANGED = 'zenml/stackChanged'; +export const LSP_ZENML_REQUIREMENTS_NOT_MET = 'zenml/requirementsNotMet'; + +// EventBus emitted events +export const LSCLIENT_READY = 'lsClientReady'; +export const LSCLIENT_STATE_CHANGED = 'lsClientStateChanged'; +export const ZENML_CLIENT_STATE_CHANGED = 'zenmlClientStateChanged'; + +export const REFRESH_ENVIRONMENT_VIEW = 'refreshEnvironmentView'; + +export const REFRESH_SERVER_STATUS = 'refreshServerStatus'; +export const SERVER_STATUS_UPDATED = 'serverStatusUpdated'; +export const ITEMS_PER_PAGE_OPTIONS = ['5', '10', '15', '20', '25', '30', '35', '40', '45', '50']; + +export const INITIAL_ZENML_SERVER_STATUS: ServerStatus = { + isConnected: false, + url: '', + dashboard_url: '', + store_type: '', + deployment_type: '', + version: '', + debug: false, + database_type: '', + secrets_store_type: '', + username: null, +}; + +export const PIPELINE_RUN_STATUS_ICONS: Record = { + initializing: 'loading~spin', + failed: 'error', + completed: 'check', + running: 'clock', + cached: 'history', +}; diff --git a/src/utils/global.ts b/src/utils/global.ts new file mode 100644 index 00000000..a4f94a3b --- /dev/null +++ b/src/utils/global.ts @@ -0,0 +1,130 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as vscode from 'vscode'; +import { PYTOOL_MODULE } from './constants'; + +/** + * Resets the ZenML Server URL and access token in the VSCode workspace configuration. + */ +export const resetGlobalConfiguration = async () => { + const config = vscode.workspace.getConfiguration('zenml'); + await config.update('serverUrl', '', vscode.ConfigurationTarget.Global); + await config.update('accessToken', '', vscode.ConfigurationTarget.Global); +}; + +/** + * Retrieves the ZenML Server URL from the VSCode workspace configuration. + */ +export const getZenMLServerUrl = (): string => { + const config = vscode.workspace.getConfiguration('zenml'); + return config.get('serverUrl') || ''; +}; + +/** + * Retrieves the ZenML access token from the VSCode workspace configuration. + */ +export const getZenMLAccessToken = (): string => { + const config = vscode.workspace.getConfiguration('zenml'); + return config.get('accessToken') || ''; +}; + +/** + * Updates the ZenML Server URL and access token in the VSCode workspace configuration. + * + * @param {string} url - The new ZenML Server URL to be updated. + * @param {string} token - The new access token to be updated. + * @returns {Promise} A promise that resolves after the configuration has been updated. + */ +export const updateServerUrlAndToken = async (url: string, token: string): Promise => { + try { + const config = vscode.workspace.getConfiguration('zenml'); + await config.update('serverUrl', url, vscode.ConfigurationTarget.Global); + await config.update('accessToken', token, vscode.ConfigurationTarget.Global); + } catch (error: any) { + console.error(`Failed to update ZenML configuration: ${error.message}`); + throw new Error('Failed to update ZenML Server URL and access token.'); + } +}; + +/** + * Updates the ZenML Server URL in the VSCode workspace configuration. + * + * @param {string} serverUrl - The new ZenML Server URL to be updated. Pass an empty string if you want to clear it. + */ +export const updateServerUrl = async (serverUrl: string): Promise => { + const config = vscode.workspace.getConfiguration('zenml'); + try { + await config.update('serverUrl', serverUrl, vscode.ConfigurationTarget.Global); + console.log('ZenML Server URL has been updated successfully.'); + } catch (error: any) { + console.error(`Failed to update ZenML configuration: ${error.message}`); + throw new Error('Failed to update ZenML Server URL.'); + } +}; + +/** + * Updates the ZenML access token in the VSCode workspace configuration. + * + * @param {string} accessToken - The new access token to be updated. Pass an empty string if you want to clear it. + */ +export const updateAccessToken = async (accessToken: string): Promise => { + const config = vscode.workspace.getConfiguration('zenml'); + try { + await config.update('accessToken', accessToken, vscode.ConfigurationTarget.Global); + console.log('ZenML access token has been updated successfully.'); + } catch (error: any) { + console.error(`Failed to update ZenML configuration: ${error.message}`); + throw new Error('Failed to update ZenML access token.'); + } +}; + +/** + * Updates the default Python interpreter path globally. + * @param interpreterPath The new default Python interpreter path. + */ +export async function updateDefaultPythonInterpreterPath(interpreterPath: string): Promise { + const config = vscode.workspace.getConfiguration('python'); + await config.update('defaultInterpreterPath', interpreterPath, vscode.ConfigurationTarget.Global); +} + +/** + * Updates the ZenML Python interpreter setting. + * @param interpreterPath The new path to the python environminterpreterent. + */ +export async function updatePytoolInterpreter(interpreterPath: string): Promise { + const config = vscode.workspace.getConfiguration(PYTOOL_MODULE); + await config.update('interpreter', [interpreterPath], vscode.ConfigurationTarget.Workspace); +} + +/** + * Retrieves the virtual environment path from the VSCode workspace configuration. + * @returns The path to the virtual environment. + */ +export function getDefaultPythonInterpreterPath(): string { + const config = vscode.workspace.getConfiguration('python'); + const defaultInterpreterPath = config.get('defaultInterpreterPath', ''); + return defaultInterpreterPath; +} + +/** + * Toggles the registration of commands for the extension. + * + * @param state The state to set the commands to. + */ +export async function toggleCommands(state: boolean): Promise { + await vscode.commands.executeCommand('setContext', 'stackCommandsRegistered', state); + await vscode.commands.executeCommand('setContext', 'componentCommandsRegistered', state); + await vscode.commands.executeCommand('setContext', 'serverCommandsRegistered', state); + await vscode.commands.executeCommand('setContext', 'pipelineCommandsRegistered', state); + await vscode.commands.executeCommand('setContext', 'environmentCommandsRegistered', state); +} diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts new file mode 100644 index 00000000..3ebe6552 --- /dev/null +++ b/src/utils/notifications.ts @@ -0,0 +1,87 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as vscode from 'vscode'; + +/** + * Shows an information message in the status bar for a specified duration. + * + * @param message The message to show. + * @param duration Duration in milliseconds after which the message will disappear. + */ +export const showStatusBarInfoMessage = (message: string, duration: number = 5000): void => { + const disposable = vscode.window.setStatusBarMessage(message); + setTimeout(() => disposable.dispose(), duration); +}; + +/** + * Shows a warning message in the status bar for a specified duration. + * + * @param message The message to show. + * @param duration Duration in milliseconds after which the message will disappear. + */ +export const showStatusBarWarningMessage = (message: string, duration: number = 5000): void => { + const disposable = vscode.window.setStatusBarMessage(`$(alert) ${message}`); + setTimeout(() => disposable.dispose(), duration); +}; + +/** + * Shows an error message in the status bar for a specified duration. + * + * @param message The message to show. + * @param duration Duration in milliseconds after which the message will disappear. + */ +export const showStatusBarErrorMessage = (message: string, duration: number = 5000): void => { + const disposable = vscode.window.setStatusBarMessage(`$(error) ${message}`); + setTimeout(() => disposable.dispose(), duration); +}; + +/** + * Shows a modal pop up information message. + * + * @param message The message to display. + */ +export const showInformationMessage = (message: string): void => { + vscode.window.showInformationMessage(message); +}; + +/** + * Shows a modal pop up error message, + * + * @param message The message to display. + */ +export const showErrorMessage = (message: string): void => { + vscode.window.showErrorMessage(message); +}; + +/** + * Shows a warning message with actions (buttons) for the user to select. + * + * @param message The warning message to display. + * @param actions An array of actions, each action being an object with a title and an action callback. + */ +export async function showWarningMessageWithActions( + message: string, + ...actions: { title: string; action: () => void | Promise }[] +): Promise { + // Map actions to their titles to display as buttons. + const items = actions.map(action => action.title); + + // Show warning message with buttons. + const selection = await vscode.window.showWarningMessage(message, ...items); + + // Find the selected action based on the title and execute its callback. + const selectedAction = actions.find(action => action.title === selection); + if (selectedAction) { + await selectedAction.action(); + } +} diff --git a/src/utils/refresh.ts b/src/utils/refresh.ts new file mode 100644 index 00000000..2f616277 --- /dev/null +++ b/src/utils/refresh.ts @@ -0,0 +1,119 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { EventBus } from '../services/EventBus'; +import { ZenServerDetails } from '../types/ServerInfoTypes'; +import { PipelineDataProvider, ServerDataProvider, StackDataProvider } from '../views/activityBar'; +import { REFRESH_SERVER_STATUS } from './constants'; + +// Type definition for a refresh function that takes a global configuration object +type RefreshFunction = (updatedServerConfig?: ZenServerDetails) => Promise; + +/** + * Debounces a function to prevent it from being called too frequently. + * + * @param func The function to debounce + * @param wait The time to wait before calling the function + * @returns A debounced version of the function + */ +export function debounce Promise>( + func: T, + wait: number +): (...args: Parameters) => Promise { + let timeout: NodeJS.Timeout | null = null; + + return async (...args: Parameters): Promise => { + return new Promise((resolve, reject) => { + const later = () => { + timeout = null; + func(...args) + .then(resolve) + .catch(reject); + }; + if (timeout) { + clearTimeout(timeout); + } + timeout = setTimeout(later, wait); + }); + }; +} + +/** + * Creates a debounced and delayed refresh function. + * + * @param refreshFn The refresh function to debounce and delay. + * @param delayMs The delay in milliseconds before executing the refresh. + * @returns A function that, when called, starts the debounced and delayed execution process. + */ +export function delayRefresh( + refreshFn: RefreshFunction, + delayMs: number = 5000 +): (updatedServerConfig?: ZenServerDetails) => void { + const debouncedRefreshFn = debounce(refreshFn, delayMs); + return (updatedServerConfig?: ZenServerDetails) => { + debouncedRefreshFn(updatedServerConfig); + }; +} + +/** + * Immediately invokes and retries it after , for a specified number of , + * applying debounce to prevent rapid successive calls. + * + * @param refreshFn The refresh function to attempt. + * @param delayMs The time in milliseconds to delay before each attempt. Default is 5s. + * @param attempts The number of attempts to make before giving up. + * @returns A function that, when called, initiates the delayed attempts to refresh. + */ + +export function delayRefreshWithRetry( + refreshFn: RefreshFunction, + delayMs: number = 5000, + attempts: number = 2 +): (updatedServerConfig?: ZenServerDetails) => void { + let refreshCount = 0; + + const executeRefresh = async (updatedServerConfig?: ZenServerDetails) => { + refreshCount++; + // refresh is called immediately + await refreshFn(updatedServerConfig); + if (refreshCount < attempts) { + setTimeout(() => executeRefresh(updatedServerConfig), delayMs); + } + }; + + const debouncedExecuteRefresh = debounce(executeRefresh, delayMs); + + return (updatedServerConfig?: ZenServerDetails) => { + debouncedExecuteRefresh(updatedServerConfig); + }; +} + +/** + * Triggers a refresh of the UI components. + * + * @returns {Promise} A promise that resolves to void. + */ +export async function refreshUIComponents(): Promise { + await ServerDataProvider.getInstance().refresh(); + await StackDataProvider.getInstance().refresh(); + await PipelineDataProvider.getInstance().refresh(); + setTimeout(() => { + EventBus.getInstance().emit(REFRESH_SERVER_STATUS); + }, 500); +} + +export const refreshUtils = { + debounce, + delayRefresh, + delayRefreshWithRetry, + refreshUIComponents, +}; diff --git a/src/views/activityBar/common/ErrorTreeItem.ts b/src/views/activityBar/common/ErrorTreeItem.ts new file mode 100644 index 00000000..8bac033a --- /dev/null +++ b/src/views/activityBar/common/ErrorTreeItem.ts @@ -0,0 +1,79 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { ThemeColor, ThemeIcon, TreeItem } from 'vscode'; + +export interface GenericErrorTreeItem { + label: string; + description: string; + message?: string; + icon?: string; +} + +export type ErrorTreeItemType = VersionMismatchTreeItem | ErrorTreeItem; + +export class ErrorTreeItem extends TreeItem { + constructor(label: string, description: string) { + super(label); + this.description = description; + this.iconPath = new ThemeIcon('warning', new ThemeColor('charts.yellow')); + } +} + +export class VersionMismatchTreeItem extends ErrorTreeItem { + constructor(clientVersion: string, serverVersion: string) { + super(`Version mismatch detected`, `Client: ${clientVersion} – Server: ${serverVersion}`); + this.iconPath = new ThemeIcon('warning', new ThemeColor('charts.yellow')); + } +} + +/** + * Creates an error item for the given error. + * + * @param error The error to create an item for. + * @returns The error tree item(s). + */ +export const createErrorItem = (error: any): TreeItem[] => { + const errorItems: TreeItem[] = []; + console.log('Creating error item', error); + if (error.clientVersion || error.serverVersion) { + errorItems.push(new VersionMismatchTreeItem(error.clientVersion, error.serverVersion)); + } + errorItems.push(new ErrorTreeItem(error.errorType || 'Error', error.message)); + return errorItems; +}; + +/** + * Creates an error item for authentication errors. + * + * @param errorMessage The error message to parse. + * @returns The error tree item(s), + */ +export const createAuthErrorItem = (errorMessage: string): ErrorTreeItem[] => { + const parts = errorMessage.split(':').map(part => part.trim()); + let [generalError, detailedError, actionSuggestion] = ['', '', '']; + + if (parts.length > 2) { + generalError = parts[0]; // "Failed to retrieve pipeline runs" + detailedError = `${parts[1]}: ${(parts[2].split('.')[0] || '').trim()}`; // "Authentication error: error decoding access token" + actionSuggestion = (parts[2].split('. ')[1] || '').trim(); // "You may need to rerun zenml connect" + } + + const errorItems: ErrorTreeItem[] = []; + if (detailedError) { + errorItems.push(new ErrorTreeItem(parts[1], detailedError.split(':')[1].trim())); + } + if (actionSuggestion) { + errorItems.push(new ErrorTreeItem(actionSuggestion, '')); + } + return errorItems; +}; diff --git a/src/views/activityBar/common/LoadingTreeItem.ts b/src/views/activityBar/common/LoadingTreeItem.ts new file mode 100644 index 00000000..df52fdb5 --- /dev/null +++ b/src/views/activityBar/common/LoadingTreeItem.ts @@ -0,0 +1,31 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { TreeItem, TreeItemCollapsibleState, ThemeIcon } from 'vscode'; + +export class LoadingTreeItem extends TreeItem { + constructor(message: string, description: string = 'Refreshing...') { + super(message, TreeItemCollapsibleState.None); + this.description = description; + this.iconPath = new ThemeIcon('sync~spin'); + } +} + +export const LOADING_TREE_ITEMS = new Map([ + ['server', new LoadingTreeItem('Refreshing Server View...')], + ['stacks', new LoadingTreeItem('Refreshing Stacks View...')], + ['components', new LoadingTreeItem('Refreshing Components View...')], + ['pipelineRuns', new LoadingTreeItem('Refreshing Pipeline Runs...')], + ['environment', new LoadingTreeItem('Refreshing Environments...')], + ['lsClient', new LoadingTreeItem('Waiting for Language Server to start...', '')], + ['zenmlClient', new LoadingTreeItem('Waiting for ZenML Client to initialize...', '')], +]); diff --git a/src/views/activityBar/common/PaginatedDataProvider.ts b/src/views/activityBar/common/PaginatedDataProvider.ts new file mode 100644 index 00000000..00732048 --- /dev/null +++ b/src/views/activityBar/common/PaginatedDataProvider.ts @@ -0,0 +1,154 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. + +import { Event, EventEmitter, TreeDataProvider, TreeItem, window } from 'vscode'; +import { ITEMS_PER_PAGE_OPTIONS } from '../../../utils/constants'; +import { CommandTreeItem } from './PaginationTreeItems'; +import { LoadingTreeItem } from './LoadingTreeItem'; +import { ErrorTreeItem } from './ErrorTreeItem'; + +/** + * Provides a base class to other DataProviders that provides all functionality + * for pagination in a tree view. + */ +export class PaginatedDataProvider implements TreeDataProvider { + protected _onDidChangeTreeData = new EventEmitter(); + readonly onDidChangeTreeData: Event = + this._onDidChangeTreeData.event; + protected pagination: { + currentPage: number; + itemsPerPage: number; + totalItems: number; + totalPages: number; + } = { + currentPage: 1, + itemsPerPage: 10, + totalItems: 0, + totalPages: 0, + }; + public items: TreeItem[] = []; + protected viewName: string = ''; + + /** + * Loads the next page. + */ + public async goToNextPage(): Promise { + try { + if (this.pagination.currentPage < this.pagination.totalPages) { + this.pagination.currentPage++; + await this.refresh(); + } + } catch (e) { + console.error(`Failed to go the next page: ${e}`); + } + } + + /** + * Loads the previous page + */ + public async goToPreviousPage(): Promise { + try { + if (this.pagination.currentPage > 1) { + this.pagination.currentPage--; + await this.refresh(); + } + } catch (e) { + console.error(`Failed to go the previous page: ${e}`); + } + } + + /** + * Sets the item count per page + */ + public async updateItemsPerPage(): Promise { + try { + const selected = await window.showQuickPick(ITEMS_PER_PAGE_OPTIONS, { + placeHolder: 'Choose the max number of items to display per page', + }); + if (selected) { + this.pagination.itemsPerPage = parseInt(selected, 10); + this.pagination.currentPage = 1; + await this.refresh(); + } + } catch (e) { + console.error(`Failed to update items per page: ${e}`); + } + } + + /** + * Refreshes the view. + */ + public async refresh(): Promise { + this._onDidChangeTreeData.fire(undefined); + } + + /** + * Returns the provided tree item. + * + * @param element element The tree item to return. + * @returns The corresponding VS Code tree item + */ + public getTreeItem(element: TreeItem): TreeItem { + return element; + } + + /** + * Gets the children of the selected element. This will insert + * PaginationTreeItems for navigation if there are other pages. + * @param {TreeItem} element The selected element + * @returns Children of the selected element + */ + public async getChildren(element?: TreeItem): Promise { + if (!element) { + if (this.items[0] instanceof LoadingTreeItem || this.items[0] instanceof ErrorTreeItem) { + return this.items; + } + return this.addPaginationCommands(this.items.slice()); + } + + if ('children' in element && Array.isArray(element.children)) { + return element.children; + } + + return undefined; + } + + private addPaginationCommands(treeItems: TreeItem[]): TreeItem[] { + const NEXT_PAGE_LABEL = 'Next Page'; + const PREVIOUS_PAGE_LABEL = 'Previous Page'; + const NEXT_PAGE_COMMAND = `zenml.next${this.viewName}Page`; + const PREVIOUS_PAGE_COMMAND = `zenml.previous${this.viewName}Page`; + + if (treeItems.length === 0 && this.pagination.currentPage === 1) { + return treeItems; + } + + if (this.pagination.currentPage < this.pagination.totalPages) { + treeItems.push( + new CommandTreeItem(NEXT_PAGE_LABEL, NEXT_PAGE_COMMAND, undefined, 'arrow-circle-right') + ); + } + + if (this.pagination.currentPage > 1) { + treeItems.unshift( + new CommandTreeItem( + PREVIOUS_PAGE_LABEL, + PREVIOUS_PAGE_COMMAND, + undefined, + 'arrow-circle-left' + ) + ); + } + return treeItems; + } +} diff --git a/src/views/activityBar/common/PaginationTreeItems.ts b/src/views/activityBar/common/PaginationTreeItems.ts new file mode 100644 index 00000000..59ce4c20 --- /dev/null +++ b/src/views/activityBar/common/PaginationTreeItems.ts @@ -0,0 +1,47 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; + +/** + * A TreeItem for displaying pagination in the VSCode TreeView. + */ +export class CommandTreeItem extends TreeItem { + constructor( + public readonly label: string, + commandId: string, + commandArguments?: any[], + icon?: string + ) { + super(label); + this.command = { + title: label, + command: commandId, + arguments: commandArguments, + }; + if (icon) { + this.iconPath = new ThemeIcon(icon); + } + } +} + +export class SetItemsPerPageTreeItem extends TreeItem { + constructor() { + super('Set items per page', TreeItemCollapsibleState.None); + this.tooltip = 'Click to set the number of items shown per page'; + this.command = { + command: 'zenml.setStacksPerPage', + title: 'Set Stack Items Per Page', + arguments: [], + }; + } +} diff --git a/src/views/activityBar/componentView/ComponentDataProvider.ts b/src/views/activityBar/componentView/ComponentDataProvider.ts new file mode 100644 index 00000000..1dda42f9 --- /dev/null +++ b/src/views/activityBar/componentView/ComponentDataProvider.ts @@ -0,0 +1,152 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { State } from 'vscode-languageclient'; +import { EventBus } from '../../../services/EventBus'; +import { LSClient } from '../../../services/LSClient'; +import { + LSCLIENT_STATE_CHANGED, + LSP_ZENML_CLIENT_INITIALIZED, + LSP_ZENML_STACK_CHANGED, +} from '../../../utils/constants'; +import { ErrorTreeItem, createErrorItem, createAuthErrorItem } from '../common/ErrorTreeItem'; +import { LOADING_TREE_ITEMS } from '../common/LoadingTreeItem'; +import { CommandTreeItem } from '../common/PaginationTreeItems'; +import { ComponentsListResponse, StackComponent } from '../../../types/StackTypes'; +import { StackComponentTreeItem } from '../stackView/StackTreeItems'; +import { PaginatedDataProvider } from '../common/PaginatedDataProvider'; + +export class ComponentDataProvider extends PaginatedDataProvider { + private static instance: ComponentDataProvider | null = null; + private eventBus = EventBus.getInstance(); + private zenmlClientReady = false; + + constructor() { + super(); + this.subscribeToEvents(); + this.items = [LOADING_TREE_ITEMS.get('components')!]; + this.viewName = 'Component'; + } + + /** + * Subscribes to relevant events to trigger a refresh of the tree view. + */ + public subscribeToEvents(): void { + this.eventBus.on(LSCLIENT_STATE_CHANGED, (newState: State) => { + if (newState === State.Running) { + this.refresh(); + } else { + this.items = [LOADING_TREE_ITEMS.get('lsClient')!]; + this._onDidChangeTreeData.fire(undefined); + } + }); + + this.eventBus.on(LSP_ZENML_CLIENT_INITIALIZED, (isInitialized: boolean) => { + this.zenmlClientReady = isInitialized; + + if (!isInitialized) { + this.items = [LOADING_TREE_ITEMS.get('components')!]; + this._onDidChangeTreeData.fire(undefined); + return; + } + this.refresh(); + this.eventBus.off(LSP_ZENML_STACK_CHANGED, () => this.refresh()); + this.eventBus.on(LSP_ZENML_STACK_CHANGED, () => this.refresh()); + }); + } + + /** + * Retrieves the singleton instance of ComponentDataProvider + * + * @returns {ComponentDataProvider} The signleton instance. + */ + public static getInstance(): ComponentDataProvider { + if (!ComponentDataProvider.instance) { + ComponentDataProvider.instance = new ComponentDataProvider(); + } + + return ComponentDataProvider.instance; + } + + /** + * Refreshes the view. + */ + public async refresh(): Promise { + this.items = [LOADING_TREE_ITEMS.get('components')!]; + this._onDidChangeTreeData.fire(undefined); + + const page = this.pagination.currentPage; + const itemsPerPage = this.pagination.itemsPerPage; + + try { + const newComponentsData = await this.fetchComponents(page, itemsPerPage); + this.items = newComponentsData; + } catch (e) { + this.items = createErrorItem(e); + } + + this._onDidChangeTreeData.fire(undefined); + } + + private async fetchComponents(page: number = 1, itemsPerPage: number = 10) { + if (!this.zenmlClientReady) { + return [LOADING_TREE_ITEMS.get('zenmlClient')!]; + } + + try { + const lsClient = LSClient.getInstance(); + const result = await lsClient.sendLsClientRequest('listComponents', [ + page, + itemsPerPage, + ]); + + if (Array.isArray(result) && result.length === 1 && 'error' in result[0]) { + const errorMessage = result[0].error; + if (errorMessage.includes('Authentication error')) { + return createAuthErrorItem(errorMessage); + } + } + + if (!result || 'error' in result) { + if ('clientVersion' in result && 'serverVersion' in result) { + return createErrorItem(result); + } else { + console.error(`Failed to fetch stack components: ${result.error}`); + return []; + } + } + + if ('items' in result) { + const { items, total, total_pages, index, max_size } = result; + this.pagination = { + currentPage: index, + itemsPerPage: max_size, + totalItems: total, + totalPages: total_pages, + }; + + const components = items.map( + (component: StackComponent) => new StackComponentTreeItem(component) + ); + return components; + } else { + console.error('Unexpected response format:', result); + return []; + } + } catch (e: any) { + console.error(`Failed to fetch components: ${e}`); + return [ + new ErrorTreeItem('Error', `Failed to fetch components: ${e.message || e.toString()}`), + ]; + } + } +} diff --git a/src/views/activityBar/environmentView/EnvironmentDataProvider.ts b/src/views/activityBar/environmentView/EnvironmentDataProvider.ts new file mode 100644 index 00000000..215773a5 --- /dev/null +++ b/src/views/activityBar/environmentView/EnvironmentDataProvider.ts @@ -0,0 +1,150 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { EventEmitter, TreeDataProvider, TreeItem } from 'vscode'; +import { State } from 'vscode-languageclient'; +import { EventBus } from '../../../services/EventBus'; +import { + LSCLIENT_STATE_CHANGED, + LSP_IS_ZENML_INSTALLED, + REFRESH_ENVIRONMENT_VIEW, + LSP_ZENML_CLIENT_INITIALIZED, +} from '../../../utils/constants'; +import { EnvironmentItem } from './EnvironmentItem'; +import { + createInterpreterDetails, + createLSClientItem, + createWorkspaceSettingsItems, + createZenMLInstallationItem, + createZenMLClientStatusItem, +} from './viewHelpers'; +import { LSNotificationIsZenMLInstalled } from '../../../types/LSNotificationTypes'; + +export class EnvironmentDataProvider implements TreeDataProvider { + private static instance: EnvironmentDataProvider | null = null; + private _onDidChangeTreeData = new EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + private lsClientStatus: State = State.Stopped; + private zenmlClientReady: boolean = false; + private zenmlInstallationStatus: LSNotificationIsZenMLInstalled | null = null; + + private eventBus = EventBus.getInstance(); + + constructor() { + this.subscribeToEvents(); + } + + private subscribeToEvents() { + this.eventBus.on(LSCLIENT_STATE_CHANGED, this.handleLsClientStateChange.bind(this)); + this.eventBus.on(LSP_ZENML_CLIENT_INITIALIZED, this.handleZenMLClientStateChange.bind(this)); + this.eventBus.on(LSP_IS_ZENML_INSTALLED, this.handleIsZenMLInstalled.bind(this)); + this.eventBus.on(REFRESH_ENVIRONMENT_VIEW, this.refresh.bind(this)); + } + + /** + * Retrieves the singleton instance of ServerDataProvider. + * + * @returns {PipelineDataProvider} The singleton instance. + */ + public static getInstance(): EnvironmentDataProvider { + if (!this.instance) { + this.instance = new EnvironmentDataProvider(); + } + return this.instance; + } + + /** + * Explicitly trigger loading state for ZenML installation check and ZenML client initialization. + */ + private triggerLoadingStateForZenMLChecks() { + this.zenmlClientReady = false; + this.zenmlInstallationStatus = null; + this.refresh(); + } + + /** + * Handles the change in the LSP client state. + * + * @param {State} status The new LSP client state. + */ + private handleLsClientStateChange(status: State) { + this.lsClientStatus = status; + if (status !== State.Running) { + this.triggerLoadingStateForZenMLChecks(); + } + this.refresh(); + } + + /** + * Handles the change in the ZenML client state. + * + * @param {boolean} isReady The new ZenML client state. + */ + private handleZenMLClientStateChange(isReady: boolean) { + this.zenmlClientReady = isReady; + this.refresh(); + } + + /** + * Handles the change in the ZenML installation status. + * + * @param {LSNotificationIsZenMLInstalled} params The new ZenML installation status. + */ + private handleIsZenMLInstalled(params: LSNotificationIsZenMLInstalled) { + this.zenmlInstallationStatus = params; + this.refresh(); + } + + /** + * Refreshes the "Pipeline Runs" view by fetching the latest pipeline run data and updating the view. + */ + public refresh(): void { + this._onDidChangeTreeData.fire(); + } + + /** + * Retrieves the tree item for a given pipeline run. + * + * @param element The pipeline run item. + * @returns The corresponding VS Code tree item. + */ + getTreeItem(element: EnvironmentItem): TreeItem { + return element; + } + + /** + * Adjusts createRootItems to set each item to not collapsible and directly return items for Interpreter, Workspace, etc. + */ + private async createRootItems(): Promise { + const items: EnvironmentItem[] = [ + createLSClientItem(this.lsClientStatus), + createZenMLInstallationItem(this.zenmlInstallationStatus), + createZenMLClientStatusItem(this.zenmlClientReady), + ...(await createInterpreterDetails()), + ...(await createWorkspaceSettingsItems()), + ]; + return items; + } + + /** + * Simplifies getChildren by always returning root items, as there are no collapsible items now. + */ + async getChildren(element?: EnvironmentItem): Promise { + if (!element) { + return this.createRootItems(); + } else { + // Since there are no collapsible items, no need to fetch children + return []; + } + } +} diff --git a/src/views/activityBar/environmentView/EnvironmentItem.ts b/src/views/activityBar/environmentView/EnvironmentItem.ts new file mode 100644 index 00000000..b43d5131 --- /dev/null +++ b/src/views/activityBar/environmentView/EnvironmentItem.ts @@ -0,0 +1,76 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import path from 'path'; +import { ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; + +export class EnvironmentItem extends TreeItem { + constructor( + public readonly label: string, + public readonly description?: string, + public readonly collapsibleState: TreeItemCollapsibleState = TreeItemCollapsibleState.None, + private readonly customIcon?: string, + public readonly contextValue?: string + ) { + super(label, collapsibleState); + this.iconPath = this.determineIcon(label); + this.contextValue = contextValue; + } + + /** + * Determines the icon for the tree item based on the label. + * + * @param label The label of the tree item. + * @returns The icon for the tree item. + */ + private determineIcon(label: string): { light: string; dark: string } | ThemeIcon | undefined { + if (this.customIcon) { + switch (this.customIcon) { + case 'check': + return new ThemeIcon('check', new ThemeColor('gitDecoration.addedResourceForeground')); + case 'close': + return new ThemeIcon('close', new ThemeColor('gitDecoration.deletedResourceForeground')); + case 'error': + return new ThemeIcon('error', new ThemeColor('errorForeground')); + case 'warning': + return new ThemeIcon('warning', new ThemeColor('charts.yellow')); + default: + return new ThemeIcon(this.customIcon); + } + } + switch (label) { + case 'Workspace': + case 'CWD': + case 'File System': + return new ThemeIcon('folder'); + case 'Interpreter': + case 'Name': + case 'Python Version': + case 'Path': + case 'EnvType': + const pythonLogo = path.join(__dirname, '..', 'resources', 'python.png'); + return { + light: pythonLogo, + dark: pythonLogo, + }; + case 'ZenML Local': + case 'ZenML Client': + const zenmlLogo = path.join(__dirname, '..', 'resources', 'logo.png'); + return { + light: zenmlLogo, + dark: zenmlLogo, + }; + default: + return undefined; + } + } +} diff --git a/src/views/activityBar/environmentView/viewHelpers.ts b/src/views/activityBar/environmentView/viewHelpers.ts new file mode 100644 index 00000000..ef3f0e28 --- /dev/null +++ b/src/views/activityBar/environmentView/viewHelpers.ts @@ -0,0 +1,178 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { TreeItemCollapsibleState } from 'vscode'; +import { EnvironmentItem } from './EnvironmentItem'; +import { getInterpreterDetails, resolveInterpreter } from '../../../common/python'; +import { getWorkspaceSettings } from '../../../common/settings'; +import { PYTOOL_MODULE } from '../../../utils/constants'; +import { getProjectRoot } from '../../../common/utilities'; +import { LSClient } from '../../../services/LSClient'; +import { State } from 'vscode-languageclient'; +import { LSNotificationIsZenMLInstalled } from '../../../types/LSNotificationTypes'; + +/** + * Creates the LSP client item for the environment view. + * + * @returns {EnvironmentItem} The LSP client item. + */ +export function createLSClientItem(lsClientStatus: State): EnvironmentItem { + const statusMappings = { + [State.Running]: { description: 'Running', icon: 'globe' }, + [State.Starting]: { description: 'Initializing…', icon: 'sync~spin' }, + [State.Stopped]: { description: 'Stopped', icon: 'close' }, + }; + + const { description, icon } = statusMappings[lsClientStatus]; + + return new EnvironmentItem( + 'LSP Client', + description, + TreeItemCollapsibleState.None, + icon, + 'lsClient' + ); +} + +/** + * Creates the ZenML status items for the environment view. + * + * @returns {EnvironmentItem} The ZenML status items. + */ +export function createZenMLClientStatusItem(zenmlClientReady: boolean): EnvironmentItem { + const localZenML = LSClient.getInstance().localZenML; + + const zenMLClientStatusItem = new EnvironmentItem( + 'ZenML Client', + !localZenML.is_installed ? '' : zenmlClientReady ? 'Initialized' : 'Awaiting Initialization', + TreeItemCollapsibleState.None, + !localZenML.is_installed ? 'warning' : zenmlClientReady ? 'check' : 'sync~spin' + ); + + return zenMLClientStatusItem; +} + +/** + * Creates the ZenML installation item for the environment view. + * + * @param installationStatus The installation status of ZenML. + * @returns {EnvironmentItem} The ZenML installation item. + */ +export function createZenMLInstallationItem( + installationStatus: LSNotificationIsZenMLInstalled | null +): EnvironmentItem { + if (!installationStatus) { + return new EnvironmentItem( + 'ZenML Local Installation', + 'Checking...', + TreeItemCollapsibleState.None, + 'sync~spin' + ); + } + + const description = installationStatus.is_installed + ? `Installed (v${installationStatus.version})` + : 'Not Installed'; + const icon = installationStatus.is_installed ? 'check' : 'warning'; + + return new EnvironmentItem( + 'ZenML Local Installation', + description, + TreeItemCollapsibleState.None, + icon + ); +} + +/** + * Creates the workspace settings items for the environment view. + * + * @returns {Promise} The workspace settings items. + */ +export async function createWorkspaceSettingsItems(): Promise { + const settings = await getWorkspaceSettings(PYTOOL_MODULE, await getProjectRoot(), true); + + return [ + new EnvironmentItem('CWD', settings.cwd), + new EnvironmentItem('File System', settings.workspace), + ...(settings.path && settings.path.length + ? [new EnvironmentItem('Path', settings.path.join('; '))] + : []), + ]; +} + +/** + * Creates the interpreter details items for the environment view. + * + * @returns {Promise} The interpreter details items. + */ +export async function createInterpreterDetails(): Promise { + const interpreterDetails = await getInterpreterDetails(); + const interpreterPath = interpreterDetails.path?.[0]; + if (!interpreterPath) { + return []; + } + + const resolvedEnv = await resolveInterpreter([interpreterPath]); + if (!resolvedEnv) { + return [ + new EnvironmentItem( + 'Details', + 'Could not resolve environment details', + TreeItemCollapsibleState.None + ), + ]; + } + const pythonVersion = `${resolvedEnv.version?.major}.${resolvedEnv.version?.minor}.${resolvedEnv.version?.micro}`; + const simplifiedPath = simplifyPath(resolvedEnv.path); + + return [ + new EnvironmentItem( + 'Python Version', + pythonVersion, + TreeItemCollapsibleState.None, + '', + 'interpreter' + ), + new EnvironmentItem( + 'Name', + resolvedEnv?.environment?.name, + TreeItemCollapsibleState.None, + '', + 'interpreter' + ), + new EnvironmentItem( + 'EnvType', + resolvedEnv?.environment?.type, + TreeItemCollapsibleState.None, + '', + 'interpreter' + ), + new EnvironmentItem('Path', simplifiedPath, TreeItemCollapsibleState.None, '', 'interpreter'), + ]; +} + +/** + * Simplifies the path by replacing the home directory with '~'. + * + * @param path The path to simplify. + * @returns {string} The simplified path. + */ +function simplifyPath(path: string): string { + if (!path) { + return ''; + } + const homeDir = process.env.HOME || process.env.USERPROFILE; + if (homeDir) { + return path.replace(homeDir, '~'); + } + return path; +} diff --git a/src/views/activityBar/index.ts b/src/views/activityBar/index.ts new file mode 100644 index 00000000..088d37a3 --- /dev/null +++ b/src/views/activityBar/index.ts @@ -0,0 +1,6 @@ +// /src/views/treeViews/index.ts +export * from './stackView/StackDataProvider'; +export * from './serverView/ServerDataProvider'; +export * from './stackView/StackTreeItems'; +export * from './pipelineView/PipelineDataProvider'; +export * from './pipelineView/PipelineTreeItems'; diff --git a/src/views/activityBar/pipelineView/PipelineDataProvider.ts b/src/views/activityBar/pipelineView/PipelineDataProvider.ts new file mode 100644 index 00000000..fc83d062 --- /dev/null +++ b/src/views/activityBar/pipelineView/PipelineDataProvider.ts @@ -0,0 +1,177 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { EventEmitter, TreeDataProvider, TreeItem, window } from 'vscode'; +import { State } from 'vscode-languageclient'; +import { EventBus } from '../../../services/EventBus'; +import { LSClient } from '../../../services/LSClient'; +import { PipelineRun, PipelineRunsResponse } from '../../../types/PipelineTypes'; +import { + ITEMS_PER_PAGE_OPTIONS, + LSCLIENT_STATE_CHANGED, + LSP_ZENML_CLIENT_INITIALIZED, + LSP_ZENML_STACK_CHANGED, +} from '../../../utils/constants'; +import { ErrorTreeItem, createErrorItem, createAuthErrorItem } from '../common/ErrorTreeItem'; +import { LOADING_TREE_ITEMS } from '../common/LoadingTreeItem'; +import { PipelineRunTreeItem, PipelineTreeItem } from './PipelineTreeItems'; +import { CommandTreeItem } from '../common/PaginationTreeItems'; +import { PaginatedDataProvider } from '../common/PaginatedDataProvider'; + +/** + * Provides data for the pipeline run tree view, displaying detailed information about each pipeline run. + */ +export class PipelineDataProvider extends PaginatedDataProvider { + private static instance: PipelineDataProvider | null = null; + private eventBus = EventBus.getInstance(); + private zenmlClientReady = false; + + constructor() { + super(); + this.items = [LOADING_TREE_ITEMS.get('pipelineRuns')!]; + this.viewName = 'PipelineRuns'; + this.subscribeToEvents(); + } + + /** + * Subscribes to relevant events to trigger a refresh of the tree view. + */ + public subscribeToEvents(): void { + this.eventBus.on(LSCLIENT_STATE_CHANGED, (newState: State) => { + if (newState === State.Running) { + this.refresh(); + } else { + this.items = [LOADING_TREE_ITEMS.get('lsClient')!]; + this._onDidChangeTreeData.fire(undefined); + } + }); + + this.eventBus.on(LSP_ZENML_CLIENT_INITIALIZED, (isInitialized: boolean) => { + this.zenmlClientReady = isInitialized; + + if (!isInitialized) { + this.items = [LOADING_TREE_ITEMS.get('pipelineRuns')!]; + this._onDidChangeTreeData.fire(undefined); + return; + } + + this.refresh(); + this.eventBus.off(LSP_ZENML_STACK_CHANGED, () => this.refresh()); + this.eventBus.on(LSP_ZENML_STACK_CHANGED, () => this.refresh()); + }); + } + + /** + * Retrieves the singleton instance of ServerDataProvider. + * + * @returns {PipelineDataProvider} The singleton instance. + */ + public static getInstance(): PipelineDataProvider { + if (!PipelineDataProvider.instance) { + PipelineDataProvider.instance = new PipelineDataProvider(); + } + return PipelineDataProvider.instance; + } + + /** + * Refreshes the "Pipeline Runs" view by fetching the latest pipeline run data and updating the view. + * + * @returns A promise resolving to void. + */ + public async refresh(): Promise { + this.items = [LOADING_TREE_ITEMS.get('pipelineRuns')!]; + this._onDidChangeTreeData.fire(undefined); + const page = this.pagination.currentPage; + const itemsPerPage = this.pagination.itemsPerPage; + + try { + const newPipelineData = await this.fetchPipelineRuns(page, itemsPerPage); + this.items = newPipelineData; + } catch (error: any) { + this.items = createErrorItem(error); + } + + this._onDidChangeTreeData.fire(undefined); + } + + /** + * Fetches pipeline runs from the server and maps them to tree items for display. + * + * @returns A promise resolving to an array of PipelineTreeItems representing fetched pipeline runs. + */ + async fetchPipelineRuns(page: number = 1, itemsPerPage: number = 20): Promise { + if (!this.zenmlClientReady) { + return [LOADING_TREE_ITEMS.get('zenmlClient')!]; + } + try { + const lsClient = LSClient.getInstance(); + const result = await lsClient.sendLsClientRequest('getPipelineRuns', [ + page, + itemsPerPage, + ]); + + if (Array.isArray(result) && result.length === 1 && 'error' in result[0]) { + const errorMessage = result[0].error; + if (errorMessage.includes('Authentication error')) { + return createAuthErrorItem(errorMessage); + } + } + + if (!result || 'error' in result) { + if ('clientVersion' in result && 'serverVersion' in result) { + return createErrorItem(result); + } else { + console.error(`Failed to fetch pipeline runs: ${result.error}`); + return []; + } + } + + if ('runs' in result) { + const { runs, total, total_pages, current_page, items_per_page } = result; + + this.pagination = { + currentPage: current_page, + itemsPerPage: items_per_page, + totalItems: total, + totalPages: total_pages, + }; + + return runs.map((run: PipelineRun) => { + const formattedStartTime = new Date(run.startTime).toLocaleString(); + const formattedEndTime = run.endTime ? new Date(run.endTime).toLocaleString() : 'N/A'; + + const children = [ + new PipelineRunTreeItem('run name', run.name), + new PipelineRunTreeItem('stack', run.stackName), + new PipelineRunTreeItem('start time', formattedStartTime), + new PipelineRunTreeItem('end time', formattedEndTime), + new PipelineRunTreeItem('os', `${run.os} ${run.osVersion}`), + new PipelineRunTreeItem('python version', run.pythonVersion), + ]; + + return new PipelineTreeItem(run, run.id, children); + }); + } else { + console.error(`Unexpected response format:`, result); + return []; + } + } catch (error: any) { + console.error(`Failed to fetch stacks: ${error}`); + return [ + new ErrorTreeItem( + 'Error', + `Failed to fetch pipeline runs: ${error.message || error.toString()}` + ), + ]; + } + } +} diff --git a/src/views/activityBar/pipelineView/PipelineTreeItems.ts b/src/views/activityBar/pipelineView/PipelineTreeItems.ts new file mode 100644 index 00000000..2b096572 --- /dev/null +++ b/src/views/activityBar/pipelineView/PipelineTreeItems.ts @@ -0,0 +1,58 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as vscode from 'vscode'; +import { PipelineRun } from '../../../types/PipelineTypes'; +import { PIPELINE_RUN_STATUS_ICONS } from '../../../utils/constants'; + +/** + * Represents a Pipeline Run Tree Item in the VS Code tree view. + * Displays its name, version and status. + */ +export class PipelineTreeItem extends vscode.TreeItem { + public children: PipelineRunTreeItem[] | undefined; + + constructor( + public readonly run: PipelineRun, + public readonly id: string, + children?: PipelineRunTreeItem[] + ) { + super( + run.name, + children === undefined + ? vscode.TreeItemCollapsibleState.None + : vscode.TreeItemCollapsibleState.Collapsed + ); + this.tooltip = `${run.name} - Status: ${run.status}`; + this.description = `status: ${run.status}`; + this.iconPath = new vscode.ThemeIcon(PIPELINE_RUN_STATUS_ICONS[run.status]); + this.children = children; + } + + contextValue = 'pipelineRun'; +} + +/** + * Represents details of a Pipeline Run Tree Item in the VS Code tree view. + * Displays the stack name for the run, its start time, end time, machine details and Python version. + */ +export class PipelineRunTreeItem extends vscode.TreeItem { + constructor( + public readonly label: string, + public readonly description: string + ) { + super(label, vscode.TreeItemCollapsibleState.None); + this.tooltip = `${label}: ${description}`; + } + + contextValue = 'pipelineRunDetail'; +} diff --git a/src/views/activityBar/serverView/ServerDataProvider.ts b/src/views/activityBar/serverView/ServerDataProvider.ts new file mode 100644 index 00000000..0b99d6ea --- /dev/null +++ b/src/views/activityBar/serverView/ServerDataProvider.ts @@ -0,0 +1,179 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { EventEmitter, ThemeColor, ThemeIcon, TreeDataProvider, TreeItem } from 'vscode'; +import { State } from 'vscode-languageclient'; +import { checkServerStatus, isServerStatus } from '../../../commands/server/utils'; +import { EventBus } from '../../../services/EventBus'; +import { ServerStatus } from '../../../types/ServerInfoTypes'; +import { + INITIAL_ZENML_SERVER_STATUS, + LSCLIENT_STATE_CHANGED, + LSP_ZENML_CLIENT_INITIALIZED, + REFRESH_SERVER_STATUS, + SERVER_STATUS_UPDATED, +} from '../../../utils/constants'; +import { LOADING_TREE_ITEMS } from '../common/LoadingTreeItem'; +import { ServerTreeItem } from './ServerTreeItems'; + +export class ServerDataProvider implements TreeDataProvider { + private _onDidChangeTreeData = new EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + private static instance: ServerDataProvider | null = null; + private eventBus = EventBus.getInstance(); + private zenmlClientReady = false; + private currentStatus: ServerStatus | TreeItem[] = INITIAL_ZENML_SERVER_STATUS; + + constructor() { + this.subscribeToEvents(); + } + + /** + * Subscribes to relevant events to trigger a refresh of the tree view. + */ + public subscribeToEvents(): void { + this.eventBus.on(LSCLIENT_STATE_CHANGED, (newState: State) => { + if (newState === State.Running) { + this.refresh(); + } else { + this.currentStatus = [LOADING_TREE_ITEMS.get('lsClient')!]; + this._onDidChangeTreeData.fire(undefined); + } + }); + + this.eventBus.on(LSP_ZENML_CLIENT_INITIALIZED, (isInitialized: boolean) => { + this.zenmlClientReady = isInitialized; + if (!isInitialized) { + this.currentStatus = [LOADING_TREE_ITEMS.get('zenmlClient')!]; + this._onDidChangeTreeData.fire(undefined); + return; + } + + this.refresh(); + this.eventBus.off(REFRESH_SERVER_STATUS, async () => await this.refresh()); + this.eventBus.on(REFRESH_SERVER_STATUS, async () => await this.refresh()); + }); + } + + /** + * Retrieves the singleton instance of ServerDataProvider. + * + * @returns {ServerDataProvider} The singleton instance. + */ + public static getInstance(): ServerDataProvider { + if (!this.instance) { + this.instance = new ServerDataProvider(); + } + return this.instance; + } + + /** + * Updates the server status to the provided status (used for tests). + * + * @param {ServerStatus} status The new server status. + */ + public updateStatus(status: ServerStatus): void { + this.currentStatus = status; + } + + /** + * Updates the server status and triggers a UI refresh to reflect the latest server status. + * If the server status has changed, it emits a serverStatusUpdated event. + * + * @returns {Promise} A promise resolving to void. + */ + public async refresh(): Promise { + this.currentStatus = [LOADING_TREE_ITEMS.get('server')!]; + this._onDidChangeTreeData.fire(undefined); + + if (!this.zenmlClientReady) { + this.currentStatus = [LOADING_TREE_ITEMS.get('zenmlClient')!]; + this._onDidChangeTreeData.fire(undefined); + return; + } + + const serverStatus = await checkServerStatus(); + if (isServerStatus(serverStatus)) { + if (JSON.stringify(serverStatus) !== JSON.stringify(this.currentStatus)) { + this.eventBus.emit(SERVER_STATUS_UPDATED, { + isConnected: serverStatus.isConnected, + serverUrl: serverStatus.url, + }); + } + } + + this.currentStatus = serverStatus; + this._onDidChangeTreeData.fire(undefined); + } + + /** + * Gets the current status of the ZenML server. + * + * @returns {ServerStatus} The current status of the ZenML server, including connectivity, host, port, store type, and store URL. + */ + public getCurrentStatus(): ServerStatus | TreeItem[] { + return this.currentStatus; + } + + /** + * Retrieves the tree item for a given element, applying appropriate icons based on the server's connectivity status. + * + * @param element The tree item to retrieve. + * @returns The corresponding VS Code tree item. + */ + getTreeItem(element: TreeItem): TreeItem { + if (element instanceof ServerTreeItem) { + if (element.serverStatus.isConnected) { + element.iconPath = new ThemeIcon( + 'vm-active', + new ThemeColor('gitDecoration.addedResourceForeground') + ); + } else { + element.iconPath = new ThemeIcon('vm-connect'); + } + } + return element; + } + + /** + * Asynchronously fetches the children for a given tree item. + * + * @param element The parent tree item. If undefined, the root server status is fetched. + * @returns A promise resolving to an array of child tree items or undefined if there are no children. + */ + async getChildren(element?: TreeItem): Promise { + if (!element) { + if (isServerStatus(this.currentStatus)) { + const updatedServerTreeItem = new ServerTreeItem('Server Status', this.currentStatus); + return [updatedServerTreeItem]; + } else if (Array.isArray(this.currentStatus)) { + return this.currentStatus; + } + } else if (element instanceof ServerTreeItem) { + return element.children; + } + return undefined; + } + + /** + * Retrieves the server version. + * + * @returns The server version. + */ + public getServerVersion(): string { + if (isServerStatus(this.currentStatus)) { + return this.currentStatus.version; + } + return 'N/A'; + } +} diff --git a/src/views/activityBar/serverView/ServerTreeItems.ts b/src/views/activityBar/serverView/ServerTreeItems.ts new file mode 100644 index 00000000..a8b2e21f --- /dev/null +++ b/src/views/activityBar/serverView/ServerTreeItems.ts @@ -0,0 +1,114 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; +import { ServerStatus } from '../../../types/ServerInfoTypes'; + +/** + * A specialized TreeItem for displaying details about a server's status. + */ +class ServerDetailTreeItem extends TreeItem { + /** + * Constructs a new ServerDetailTreeItem instance. + * + * @param {string} label The detail label. + * @param {string} detail The detail value. + * @param {string} [iconName] The name of the icon to display. + */ + constructor(label: string, detail: string, iconName?: string) { + super(`${label}: ${detail}`, TreeItemCollapsibleState.None); + if (iconName) { + this.iconPath = new ThemeIcon(iconName); + } + + if (detail.startsWith('http://') || detail.startsWith('https://')) { + this.command = { + title: 'Open URL', + command: 'vscode.open', + arguments: [Uri.parse(detail)], + }; + this.iconPath = new ThemeIcon('link', new ThemeColor('textLink.foreground')); + this.tooltip = `Click to open ${detail}`; + } + } +} + +/** + * TreeItem for representing and visualizing server status in a tree view. Includes details such as connectivity, + * host, and port as children items when connected, or storeType and storeUrl when disconnected. + */ +export class ServerTreeItem extends TreeItem { + public children: TreeItem[] | ServerDetailTreeItem[] | undefined; + + constructor( + public readonly label: string, + public readonly serverStatus: ServerStatus + ) { + super( + label, + serverStatus.isConnected + ? TreeItemCollapsibleState.Expanded + : TreeItemCollapsibleState.Expanded + ); + + this.description = `${this.serverStatus.isConnected ? 'Connected ✅' : 'Disconnected'}`; + this.children = this.determineChildrenBasedOnStatus(); + } + + private determineChildrenBasedOnStatus(): ServerDetailTreeItem[] { + const children: ServerDetailTreeItem[] = [ + new ServerDetailTreeItem('URL', this.serverStatus.url, 'link'), + new ServerDetailTreeItem('Dashboard URL', this.serverStatus.dashboard_url, 'link'), + new ServerDetailTreeItem('Version', this.serverStatus.version, 'versions'), + new ServerDetailTreeItem('Store Type', this.serverStatus.store_type || 'N/A', 'database'), + new ServerDetailTreeItem('Deployment Type', this.serverStatus.deployment_type, 'rocket'), + new ServerDetailTreeItem('Database Type', this.serverStatus.database_type, 'database'), + new ServerDetailTreeItem('Secrets Store Type', this.serverStatus.secrets_store_type, 'lock'), + ]; + + // Conditional children based on server status type + if (this.serverStatus.id) { + children.push(new ServerDetailTreeItem('ID', this.serverStatus.id, 'key')); + } + if (this.serverStatus.username) { + children.push(new ServerDetailTreeItem('Username', this.serverStatus.username, 'account')); + } + if (this.serverStatus.debug !== undefined) { + children.push( + new ServerDetailTreeItem('Debug', this.serverStatus.debug ? 'true' : 'false', 'bug') + ); + } + if (this.serverStatus.auth_scheme) { + children.push( + new ServerDetailTreeItem('Auth Scheme', this.serverStatus.auth_scheme, 'shield') + ); + } + // Specific to SQL Server Status + if (this.serverStatus.database) { + children.push(new ServerDetailTreeItem('Database', this.serverStatus.database, 'database')); + } + if (this.serverStatus.backup_directory) { + children.push( + new ServerDetailTreeItem('Backup Directory', this.serverStatus.backup_directory, 'folder') + ); + } + if (this.serverStatus.backup_strategy) { + children.push( + new ServerDetailTreeItem('Backup Strategy', this.serverStatus.backup_strategy, 'shield') + ); + } + + return children; + } + + contextValue = 'server'; +} diff --git a/src/views/activityBar/stackView/StackDataProvider.ts b/src/views/activityBar/stackView/StackDataProvider.ts new file mode 100644 index 00000000..b611767d --- /dev/null +++ b/src/views/activityBar/stackView/StackDataProvider.ts @@ -0,0 +1,189 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { Event, EventEmitter, TreeDataProvider, TreeItem, window, workspace } from 'vscode'; +import { State } from 'vscode-languageclient'; +import { EventBus } from '../../../services/EventBus'; +import { LSClient } from '../../../services/LSClient'; +import { Stack, StackComponent, StacksResponse } from '../../../types/StackTypes'; +import { + ITEMS_PER_PAGE_OPTIONS, + LSCLIENT_STATE_CHANGED, + LSP_ZENML_CLIENT_INITIALIZED, + LSP_ZENML_STACK_CHANGED, +} from '../../../utils/constants'; +import { ErrorTreeItem, createErrorItem, createAuthErrorItem } from '../common/ErrorTreeItem'; +import { LOADING_TREE_ITEMS } from '../common/LoadingTreeItem'; +import { StackComponentTreeItem, StackTreeItem } from './StackTreeItems'; +import { CommandTreeItem } from '../common/PaginationTreeItems'; +import { PaginatedDataProvider } from '../common/PaginatedDataProvider'; + +export class StackDataProvider extends PaginatedDataProvider { + private static instance: StackDataProvider | null = null; + private eventBus = EventBus.getInstance(); + private zenmlClientReady = false; + + constructor() { + super(); + this.subscribeToEvents(); + this.items = [LOADING_TREE_ITEMS.get('stacks')!]; + this.viewName = 'Stack'; + } + + /** + * Subscribes to relevant events to trigger a refresh of the tree view. + */ + public subscribeToEvents(): void { + this.eventBus.on(LSCLIENT_STATE_CHANGED, (newState: State) => { + if (newState === State.Running) { + this.refresh(); + } else { + this.items = [LOADING_TREE_ITEMS.get('lsClient')!]; + this._onDidChangeTreeData.fire(undefined); + } + }); + + this.eventBus.on(LSP_ZENML_CLIENT_INITIALIZED, (isInitialized: boolean) => { + this.zenmlClientReady = isInitialized; + + if (!isInitialized) { + this.items = [LOADING_TREE_ITEMS.get('stacks')!]; + this._onDidChangeTreeData.fire(undefined); + return; + } + this.refresh(); + this.eventBus.off(LSP_ZENML_STACK_CHANGED, () => this.refresh()); + this.eventBus.on(LSP_ZENML_STACK_CHANGED, () => this.refresh()); + }); + } + + /** + * Retrieves the singleton instance of ServerDataProvider. + * + * @returns {StackDataProvider} The singleton instance. + */ + public static getInstance(): StackDataProvider { + if (!this.instance) { + this.instance = new StackDataProvider(); + } + return this.instance; + } + + /** + * Refreshes the tree view data by refetching stacks and triggering the onDidChangeTreeData event. + * + * @returns {Promise} A promise that resolves when the tree view data has been refreshed. + */ + public async refresh(): Promise { + this.items = [LOADING_TREE_ITEMS.get('stacks')!]; + this._onDidChangeTreeData.fire(undefined); + + const page = this.pagination.currentPage; + const itemsPerPage = this.pagination.itemsPerPage; + + try { + const newStacksData = await this.fetchStacksWithComponents(page, itemsPerPage); + this.items = newStacksData; + } catch (error: any) { + this.items = createErrorItem(error); + } + + this._onDidChangeTreeData.fire(undefined); + } + + /** + * Retrieves detailed stack information, including components, from the server. + * + * @returns {Promise} A promise that resolves with an array of `StackTreeItem` objects. + */ + async fetchStacksWithComponents( + page: number = 1, + itemsPerPage: number = 20 + ): Promise { + if (!this.zenmlClientReady) { + return [LOADING_TREE_ITEMS.get('zenmlClient')!]; + } + + try { + const lsClient = LSClient.getInstance(); + const result = await lsClient.sendLsClientRequest('fetchStacks', [ + page, + itemsPerPage, + ]); + + if (Array.isArray(result) && result.length === 1 && 'error' in result[0]) { + const errorMessage = result[0].error; + if (errorMessage.includes('Authentication error')) { + return createAuthErrorItem(errorMessage); + } + } + + if (!result || 'error' in result) { + if ('clientVersion' in result && 'serverVersion' in result) { + return createErrorItem(result); + } else { + console.error(`Failed to fetch stacks: ${result.error}`); + return []; + } + } + + if ('stacks' in result) { + const { stacks, total, total_pages, current_page, items_per_page } = result; + + this.pagination = { + currentPage: current_page, + itemsPerPage: items_per_page, + totalItems: total, + totalPages: total_pages, + }; + + return stacks.map((stack: Stack) => + this.convertToStackTreeItem(stack, this.isActiveStack(stack.id)) + ); + } else { + console.error(`Unexpected response format:`, result); + return []; + } + } catch (error: any) { + console.error(`Failed to fetch stacks: ${error}`); + return [ + new ErrorTreeItem('Error', `Failed to fetch stacks: ${error.message || error.toString()}`), + ]; + } + } + + /** + * Helper method to determine if a stack is the active stack. + * + * @param {string} stackId The ID of the stack. + * @returns {boolean} True if the stack is active; otherwise, false. + */ + private isActiveStack(stackId: string): boolean { + const activeStackId = workspace.getConfiguration('zenml').get('activeStackId'); + return stackId === activeStackId; + } + + /** + * Transforms a stack from the API into a `StackTreeItem` with component sub-items. + * + * @param {any} stack - The stack object fetched from the API. + * @returns {StackTreeItem} A `StackTreeItem` object representing the stack and its components. + */ + private convertToStackTreeItem(stack: Stack, isActive: boolean): StackTreeItem { + const componentTreeItems = Object.entries(stack.components).flatMap(([type, componentsArray]) => + componentsArray.map( + (component: StackComponent) => new StackComponentTreeItem(component, stack.id) + ) + ); + return new StackTreeItem(stack.name, stack.id, componentTreeItems, isActive); + } +} diff --git a/src/views/activityBar/stackView/StackTreeItems.ts b/src/views/activityBar/stackView/StackTreeItems.ts new file mode 100644 index 00000000..ea54c802 --- /dev/null +++ b/src/views/activityBar/stackView/StackTreeItems.ts @@ -0,0 +1,58 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import * as vscode from 'vscode'; +import { StackComponent } from '../../../types/StackTypes'; + +/** + * A TreeItem for displaying a stack in the VSCode TreeView. + * This item can be expanded to show the components of the stack. + */ +export class StackTreeItem extends vscode.TreeItem { + public children: vscode.TreeItem[] | undefined; + public isActive: boolean; + + constructor( + public readonly label: string, + public readonly id: string, + components: StackComponentTreeItem[], + isActive?: boolean + ) { + super(label, vscode.TreeItemCollapsibleState.Collapsed); + this.children = components; + this.contextValue = 'stack'; + this.isActive = isActive || false; + + if (isActive) { + this.iconPath = new vscode.ThemeIcon('pass-filled', new vscode.ThemeColor('charts.green')); + } + } +} + +/** + * A TreeItem for displaying a stack component in the VSCode TreeView. + */ +export class StackComponentTreeItem extends vscode.TreeItem { + constructor( + public component: StackComponent, + public stackId?: string + ) { + super(component.name, vscode.TreeItemCollapsibleState.None); + + this.tooltip = stackId + ? `Type: ${component.type}, Flavor: ${component.flavor}, ID: ${stackId}` + : `Type: ${component.type}, Flavor: ${component.flavor}`; + this.description = `${component.type} (${component.flavor})`; + this.contextValue = 'stackComponent'; + this.id = stackId ? `${stackId}-${component.id}` : `${component.id}`; + } +} diff --git a/src/views/panel/panelView/PanelDataProvider.ts b/src/views/panel/panelView/PanelDataProvider.ts new file mode 100644 index 00000000..ec7c4451 --- /dev/null +++ b/src/views/panel/panelView/PanelDataProvider.ts @@ -0,0 +1,103 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { EventEmitter, TreeDataProvider, TreeItem } from 'vscode'; +import { + JsonObject, + PanelDetailTreeItem, + PanelTreeItem, + SourceCodeTreeItem, +} from './PanelTreeItem'; + +import { LoadingTreeItem } from '../../activityBar/common/LoadingTreeItem'; + +export class PanelDataProvider implements TreeDataProvider { + private _onDidChangeTreeData = new EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + private static instance: PanelDataProvider | null = null; + private data: JsonObject | TreeItem = new TreeItem( + 'No data has been requested for visualization yet' + ); + private dataType: string = ''; + + /** + * Retrieves the singleton instance of PanelDataProvider + * @returns {PanelDataProvider} The singleton instance + */ + public static getInstance(): PanelDataProvider { + if (!PanelDataProvider.instance) { + PanelDataProvider.instance = new PanelDataProvider(); + } + + return PanelDataProvider.instance; + } + + /** + * Refreshes the ZenML Panel View + */ + public refresh(): void { + this._onDidChangeTreeData.fire(undefined); + } + + /** + * Sets the data to be viewed in the ZenML Panel View + * @param data Data to visualize + * @param dataType Type of data being visualized + */ + public setData(data: JsonObject, dataType = 'data'): void { + this.data = data; + this.dataType = dataType; + this.refresh(); + } + + public setLoading(): void { + this.data = new LoadingTreeItem('Retrieving data'); + this.refresh(); + } + + /** + * Retrieves the tree item for a given data property + * + * @param element The data property. + * @returns The corresponding VS Code tree item. + */ + public getTreeItem(element: TreeItem): TreeItem | Thenable { + return element; + } + + /** + * Retrieves the children for a given tree item. + * + * @param element The parent tree item. If undefined, a PanelTreeItem is created + * @returns An array of child tree items or undefined if there are no children. + */ + public getChildren(element?: TreeItem | undefined): TreeItem[] | undefined { + if (element) { + if ( + element instanceof PanelTreeItem || + element instanceof PanelDetailTreeItem || + element instanceof SourceCodeTreeItem + ) { + return element.children; + } + + return undefined; + } + + if (this.data instanceof TreeItem) { + return [this.data]; + } + + return [new PanelTreeItem(this.dataType, this.data)]; + } +} diff --git a/src/views/panel/panelView/PanelTreeItem.ts b/src/views/panel/panelView/PanelTreeItem.ts new file mode 100644 index 00000000..b6e6777b --- /dev/null +++ b/src/views/panel/panelView/PanelTreeItem.ts @@ -0,0 +1,87 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; + +type JsonType = string | number | Array | JsonObject; + +export interface JsonObject { + [key: string]: JsonType; +} + +export class PanelDetailTreeItem extends TreeItem { + public children: PanelDetailTreeItem[] = []; + + /** + * Constructs a PanelDetailTreeItem object + * @param key Property key for TreeItem + * @param value Property value for the TreeItem + */ + constructor(key: string, value: JsonType) { + const simpleValue = typeof value === 'string' || typeof value === 'number'; + super(key, simpleValue ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed); + if (simpleValue) { + this.description = String(value); + } else if (value) { + this.description = '...'; + this.children = Object.entries(value).map( + ([key, value]) => new PanelDetailTreeItem(key, value) + ); + } + + if (typeof value === 'string' && value.startsWith('http')) { + this.command = { + title: 'Open URL', + command: 'vscode.open', + arguments: [Uri.parse(value)], + }; + this.iconPath = new ThemeIcon('link', new ThemeColor('textLink.foreground')); + this.tooltip = `Click to open ${value}`; + } + } +} + +export class PanelTreeItem extends TreeItem { + public children: Array = []; + + /** + * Constructs a PanelTreeItem + * @param label Data Type Label for the PanelTreeItem + * @param data Object Data to build children + */ + constructor(label: string, data: JsonObject) { + super(label, TreeItemCollapsibleState.Expanded); + this.children = Object.entries(data).map(([key, value]) => { + if (key === 'sourceCode' && typeof value === 'string') { + return new SourceCodeTreeItem(key, value); + } + return new PanelDetailTreeItem(key, value); + }); + } +} + +export class SourceCodeTreeItem extends TreeItem { + public children: TreeItem[] = []; + + /** + * Constructs a SourceCodeTreeItem that builds its childrens based on string passed to it + * @param label Property Label for parent object + * @param sourceCode Raw string of source code + */ + constructor(label: string, sourceCode: string) { + super(label, TreeItemCollapsibleState.Collapsed); + this.description = '...'; + + const lines = sourceCode.split('\n'); + this.children = lines.map(line => new TreeItem(line)); + } +} diff --git a/src/views/statusBar/index.ts b/src/views/statusBar/index.ts new file mode 100644 index 00000000..d931a52e --- /dev/null +++ b/src/views/statusBar/index.ts @@ -0,0 +1,182 @@ +// Copyright(c) ZenML GmbH 2024. All Rights Reserved. +// Licensed under the Apache License, Version 2.0(the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied.See the License for the specific language governing +// permissions and limitations under the License. +import { QuickPickItemKind, StatusBarAlignment, StatusBarItem, commands, window } from 'vscode'; +import { getActiveStack, switchActiveStack } from '../../commands/stack/utils'; +import { EventBus } from '../../services/EventBus'; +import { LSP_ZENML_STACK_CHANGED, SERVER_STATUS_UPDATED } from '../../utils/constants'; +import { StackDataProvider } from '../activityBar'; +import { ErrorTreeItem } from '../activityBar/common/ErrorTreeItem'; + +/** + * Represents the ZenML extension's status bar. + * This class manages two main status indicators: the server status and the active stack name. + */ +export default class ZenMLStatusBar { + private static instance: ZenMLStatusBar; + private statusBarItem: StatusBarItem; + private serverStatus = { isConnected: false, serverUrl: '' }; + private activeStack: string = '$(loading~spin) Loading...'; + private activeStackId: string = ''; + private eventBus = EventBus.getInstance(); + + /** + * Initializes a new instance of the ZenMLStatusBar class. + * Sets up the status bar items for server status and active stack, subscribes to server status updates, + * and initiates the initial refresh of the status bar state. + */ + constructor() { + this.statusBarItem = window.createStatusBarItem(StatusBarAlignment.Right, 100); + this.subscribeToEvents(); + this.statusBarItem.command = 'zenml/statusBar/switchStack'; + } + + /** + * Registers the commands associated with the ZenMLStatusBar. + */ + public registerCommands(): void { + commands.registerCommand('zenml/statusBar/switchStack', () => this.switchStack()); + } + + /** + * Subscribes to relevant events to trigger a refresh of the status bar. + * + * @returns void + */ + private subscribeToEvents(): void { + this.eventBus.on(LSP_ZENML_STACK_CHANGED, async () => { + await this.refreshActiveStack(); + }); + + this.eventBus.on(SERVER_STATUS_UPDATED, ({ isConnected, serverUrl }) => { + this.updateStatusBarItem(isConnected); + this.serverStatus = { isConnected, serverUrl }; + }); + } + + /** + * Retrieves or creates an instance of the ZenMLStatusBar. + * This method implements the Singleton pattern to ensure that only one instance of ZenMLStatusBar exists. + * + * @returns {ZenMLStatusBar} The singleton instance of the ZenMLStatusBar. + */ + public static getInstance(): ZenMLStatusBar { + if (!ZenMLStatusBar.instance) { + ZenMLStatusBar.instance = new ZenMLStatusBar(); + } + return ZenMLStatusBar.instance; + } + + /** + * Asynchronously refreshes the active stack display in the status bar. + * Attempts to retrieve the current active stack name and updates the status bar item accordingly. + */ + public async refreshActiveStack(): Promise { + this.statusBarItem.text = `$(loading~spin) Loading...`; + this.statusBarItem.show(); + + try { + const activeStack = await getActiveStack(); + this.activeStackId = activeStack?.id || ''; + this.activeStack = activeStack?.name || 'default'; + } catch (error) { + console.error('Failed to fetch active ZenML stack:', error); + this.activeStack = 'Error'; + } + this.updateStatusBarItem(this.serverStatus.isConnected); + } + + /** + * Updates the status bar item with the server status and active stack information. + * + * @param {boolean} isConnected Whether the server is currently connected. + */ + private updateStatusBarItem(isConnected: boolean) { + this.statusBarItem.text = this.activeStack.includes('loading') + ? this.activeStack + : `⛩ ${this.activeStack}`; + const serverStatusText = isConnected ? 'Connected ✅' : 'Disconnected'; + this.statusBarItem.tooltip = `Server Status: ${serverStatusText}\nActive Stack: ${this.activeStack}\n(click to switch stacks)`; + this.statusBarItem.show(); + } + + /** + * Switches the active stack by prompting the user to select a stack from the available options. + * + * @returns {Promise} A promise that resolves when the active stack has been successfully switched. + */ + private async switchStack(): Promise { + const stackDataProvider = StackDataProvider.getInstance(); + const { items } = stackDataProvider; + + const containsErrors = items.some(stack => stack instanceof ErrorTreeItem); + + if (containsErrors || items.length === 0) { + window.showErrorMessage('No stacks available.'); + return; + } + + const activeStack = items.find(stack => stack.id === this.activeStackId); + const otherStacks = items.filter(stack => stack.id !== this.activeStackId); + + const quickPickItems = [ + { + label: 'Current Active', + kind: QuickPickItemKind.Separator, + }, + { + label: activeStack?.label as string, + id: activeStack?.id, + kind: QuickPickItemKind.Default, + disabled: true, + }, + ...otherStacks.map(stack => ({ + id: stack.id, + label: stack.label as string, + kind: QuickPickItemKind.Default, + })), + ]; + + // Temporarily disable the tooltip to prevent it from appearing after making a selection + this.statusBarItem.tooltip = undefined; + + const selectedStack = await window.showQuickPick(quickPickItems, { + placeHolder: 'Select a stack to switch to', + matchOnDescription: true, + matchOnDetail: true, + ignoreFocusOut: false, + }); + + if (selectedStack && selectedStack.id !== this.activeStackId) { + this.statusBarItem.text = `$(loading~spin) Switching...`; + this.statusBarItem.show(); + + const stackId = otherStacks.find(stack => stack.label === selectedStack.label)?.id; + if (stackId) { + await switchActiveStack(stackId); + await StackDataProvider.getInstance().refresh(); + this.activeStackId = stackId; + this.activeStack = selectedStack.label; + this.statusBarItem.text = `⛩ ${selectedStack.label}`; + } + } + + this.statusBarItem.hide(); + setTimeout(() => { + const serverStatusText = this.serverStatus.isConnected + ? 'Connected ✅' + : 'Disconnected (local)'; + this.statusBarItem.tooltip = `Server Status: ${serverStatusText}\nActive Stack: ${this.activeStack}\n(click to switch stacks)`; + this.statusBarItem.show(); + }, 0); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..45e4ce9b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "Node16", + "target": "ES2022", + "lib": ["ES2022"], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "skipLibCheck": true + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 00000000..30669df8 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,59 @@ +//@ts-check + +'use strict'; + +const path = require('path'); + +//@ts-check +/** @typedef {import('webpack').Configuration} WebpackConfig **/ + +/** @type WebpackConfig */ +const extensionConfig = { + target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ + mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') + + entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ + output: { + // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ + path: path.resolve(__dirname, 'dist'), + filename: 'extension.js', + libraryTarget: 'commonjs2', + }, + externals: { + vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ + // modules added here also need to be added in the .vscodeignore file + }, + resolve: { + // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader + extensions: ['.ts', '.js'], + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: /node_modules/, + use: [ + { + loader: 'ts-loader', + }, + ], + }, + ], + }, + devtool: 'nosources-source-map', + infrastructureLogging: { + level: 'log', // enables logging required for problem matchers + }, +}; + +const dagWebviewConfig = { + target: 'web', + mode: 'none', + entry: './resources/dag-view/dag.js', + output: { + path: path.resolve(__dirname, 'resources', 'dag-view'), + filename: 'dag-packed.js', + }, +}; + +module.exports = [extensionConfig, dagWebviewConfig];