diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index a004fd79..6bdea1a8 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -95,7 +95,7 @@ jobs: - name: Install OctoRelay run: pip install -e . - name: Prepare testing environment - run: pip install coverage pylint snapshottest + run: pip install coverage pylint snapshottest mypy - name: Test working-directory: tests run: python -m coverage run -m unittest test*.py @@ -103,7 +103,7 @@ jobs: working-directory: tests run: | python -m coverage lcov --omit=test*,_version* - python -m coverage report --show-missing --omit=test*,_version* + python -m coverage report --show-missing --omit=test*,_version*,snap_* - name: Coveralls uses: coverallsapp/github-action@v2 continue-on-error: true @@ -112,6 +112,8 @@ jobs: flag-name: python-${{ matrix.python }}-octoprint-${{ matrix.octoprint }} parallel: true file: ./tests/coverage.lcov + - name: Types checking + run: mypy ./octoprint_octorelay - name: Lint run: pylint --rcfile=.pylintrc ./octoprint_octorelay ./tests diff --git a/.gitignore b/.gitignore index 5e6b70c0..6ae254cf 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ OctoRelay.egg-info .coverage coverage.lcov octorelay.js +octorelay.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 94d1c3d9..ea51bb56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,39 @@ ## Version 3 +### 3.14.0 + +- UI: Generating CSS from SCSS. +- Using `mypy` for type constraints. +- Introducing the plugin API v2. + - This feature aims to establish consistency and improve clarity across inputs and responses of the plugin API. + - This feature is opt-in until the next major release of the plugin. + - Developers and other API users are advised to migrate to the new API. + - Introducing the `version` and its shorthand `v` parameter of each API request payload (integer). + - When the parameter is ommitted, the API falls back to v1 (previous behaviour and responses). + - When the parameter is set to `2`, the new request payload is expected and the API responds differently. + - Main differences of v2: + - For `update` and `getStatus` commands `pin` parameter is renamed to `subject`. + - For `listAllStatus` command `active` property renamed to `status`. + - For `getStatus` command on disabled relay the API responds with an HTTP code `400` instead of `status: false`. + - For `update` and `cancelTask` commands there is no more `status: "ok" | "error"`: + - The `status` now always means the relay state (boolean), while errors are reported via HTTP codes. + - Check out the updated Readme for new examples. + +| Command | v1 request parameters | v2 request parameter | +|---------------|--------------------------|--------------------------| +| update | `pin, target` | `subject, target` | +| getStatus | `pin` | `subject` | +| listAllStatus | None | None | +| cancelTask | `subject, target, owner` | `subject, target, owner` | + +| Command | v1 response | v2 response | +|---------------|----------------------------|----------------------| +| update | `result: bool, status: ok` | `status: bool` | +| getStatus | `status` | `status` | +| listAllStatus | `[active, id, name]` | `[status, id, name]` | +| cancelTask | `status: ok` | `cancelled: bool` | + ### 3.12.0 - Feature: the API command `update` now accepts the optional `target` parameter. diff --git a/MANIFEST.in b/MANIFEST.in index 424f0ab1..1014e508 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,4 +4,5 @@ recursive-include octoprint_octorelay/templates * recursive-include octoprint_octorelay/translations * recursive-include octoprint_octorelay/static * exclude octoprint_octorelay/static/js/.gitkeep +exclude octoprint_octorelay/static/css/.gitkeep prune tests diff --git a/README.md b/README.md index 02564abf..2ff6af02 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ You can toggle the relays ON and OFF the following ways: ## OctoRelay API Relays can be queried and updated through the [OctoPrint API](https://docs.octoprint.org/en/master/api/). Read that documentation on how to get an API Key. +Each API request payload is required to have `version` or `v` entry, meaning the API version as described below. ### Change the relay state @@ -95,16 +96,15 @@ curl 'http://octopi.local/api/plugin/octorelay' \ -H 'X-Api-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -X POST \ - -d '{ "command": "update", "pin": "r1", "target": false }' + -d '{ "v": 2, "command": "update", "subject": "r1", "target": false }' # Sample response: # { -# "result": false, -# "status": "ok" +# "status": false # } ``` -The `target` entry in request payload is an optional boolean parameter. When it's `null` or omitted the relay will toggle. The `result` entry in the response payload reflects the relay state as the outcome of the request. +The `target` entry in request payload is an optional boolean parameter. When it's `null` or omitted the relay will toggle. The `status` entry in the response payload reflects the relay state as the outcome of the request. ### Request the relay state @@ -115,7 +115,7 @@ curl 'http://octopi.local/api/plugin/octorelay' \ -H 'X-Api-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -X POST \ - -d '{ "command": "getStatus", "pin": "r1" }' + -d '{ "v": 2, "command": "getStatus", "subject": "r1" }' # Sample response: # { @@ -132,24 +132,24 @@ curl 'http://octopi.local/api/plugin/octorelay' \ -H 'X-Api-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -X POST \ - -d '{ "command": "listAllStatus" }' + -d '{ "v": 2, "command": "listAllStatus" }' # Sample response: # [ # { -# "active": true, +# "status": true, # "id": "r1", # "name": "Light" # }, # { -# "active": false, +# "status": false, # "id": "r2", # "name": "Printer" # } # ] ``` -The `active` entry reflects the actual state of the relay. +The `status` entry reflects the actual state of the relay. ## Updates diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..adb21582 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,14 @@ +[mypy] +check_untyped_defs = True +exclude = (?x)( + _version\.py$ + ) + +[mypy-octoprint.*] +ignore_missing_imports = True + +[mypy-RPi.*] +ignore_missing_imports = True + +[mypy-flask.*] +ignore_missing_imports = True diff --git a/octoprint_octorelay/__init__.py b/octoprint_octorelay/__init__.py index 141affe8..3664e34d 100755 --- a/octoprint_octorelay/__init__.py +++ b/octoprint_octorelay/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals -from typing import Optional +from typing import Optional, List, Dict, Iterable from functools import reduce import os import time @@ -19,7 +19,10 @@ ) from .driver import Relay from .task import Task +from .listing import Listing from .migrations import migrate +from .model import Model, get_initial_model +from .exceptions import HandlingException # pylint: disable=too-many-ancestors # pylint: disable=too-many-instance-attributes @@ -38,8 +41,8 @@ class OctoRelayPlugin( def __init__(self): # pylint: disable=super-init-not-called self.polling_timer = None - self.tasks = [] # of Task - self.model = { index: {} for index in RELAY_INDEXES } + self.tasks: List[Task] = [] + self.model: Model = get_initial_model() def get_settings_version(self): return SETTINGS_VERSION @@ -77,13 +80,14 @@ def on_after_startup(self): def on_shutdown(self): self._logger.debug("Stopping the plugin") - self.polling_timer.cancel() + if self.polling_timer: + self.polling_timer.cancel() self._logger.info("The plugin stopped") def get_api_commands(self): return { - UPDATE_COMMAND: [ "pin" ], - GET_STATUS_COMMAND: [ "pin" ], + UPDATE_COMMAND: [], + GET_STATUS_COMMAND: [], LIST_ALL_COMMAND: [], CANCEL_TASK_COMMAND: [ "subject", "target", "owner" ] } @@ -98,9 +102,9 @@ def has_switch_permission(self): self._logger.warn(f"Failed to check relay switching permission, {exception}") return False - def handle_list_all_command(self): + def handle_list_all_command(self) -> Listing: self._logger.debug("Collecting information on all the relay states") - active_relays = [] + active_relays: Listing = [] settings = self._settings.get([], merged=True) # expensive for index in RELAY_INDEXES: if bool(settings[index]["active"]): @@ -111,58 +115,94 @@ def handle_list_all_command(self): active_relays.append({ "id": index, "name": settings[index]["label_text"], - "active": relay.is_closed(), + "status": relay.is_closed(), }) - self._logger.debug(f"Responding to {LIST_ALL_COMMAND} command: {active_relays}") - return flask.jsonify(active_relays) + return active_relays - def handle_get_status_command(self, index: str): + def handle_get_status_command(self, index: str) -> bool: self._logger.debug(f"Getting the relay {index} state") settings = self._settings.get([index], merged=True) # expensive - is_closed = Relay( + if not bool(settings["active"]): + raise HandlingException(400) + return Relay( int(settings["relay_pin"] or 0), bool(settings["inverted_output"]) - ).is_closed() if bool(settings["active"]) else False - self._logger.debug(f"Responding to {GET_STATUS_COMMAND} command: {is_closed}") - return flask.jsonify(status=is_closed) + ).is_closed() - def handle_update_command(self, index: str, target: Optional[bool] = None): + def handle_update_command(self, index: str, target: Optional[bool] = None) -> bool: self._logger.debug(f"Requested to switch the relay {index} to {target}") if not self.has_switch_permission(): self._logger.warn("Insufficient permissions") - return flask.abort(403) + raise HandlingException(403) if index not in RELAY_INDEXES: self._logger.warn(f"Invalid relay index supplied: {index}") - return flask.jsonify(status="error") + raise HandlingException(400) try: state = self.toggle_relay(index, target) - except Exception as exception: - self._logger.warn(f"Failed to toggle the relay {index}, reason: {exception}") - return flask.jsonify(status="error", reason=f"Can not toggle the relay {index}") + except Exception as exception: # disabled relay + raise HandlingException(400) from exception self.update_ui() - self._logger.debug(f"Responding to {UPDATE_COMMAND} command. Switched state to {state}") - return flask.jsonify(status="ok", result=state) + return state - def handle_cancel_task_command(self, subject: str, target: bool, owner: str): + def handle_cancel_task_command(self, subject: str, target: bool, owner: str) -> bool: self._logger.debug(f"Cancelling tasks from {owner} to switch the relay {subject} {'ON' if target else 'OFF'}") - self.cancel_tasks(subject, USER_ACTION, target, owner) + is_cancelled = self.cancel_tasks(subject, USER_ACTION, target, owner) self.update_ui() - self._logger.debug(f"Responding to {CANCEL_TASK_COMMAND} command") - return flask.jsonify(status="ok") + return is_cancelled def on_api_command(self, command, data): + # pylint: disable=too-many-return-statements self._logger.info(f"Received the API command {command} with parameters: {data}") - if command == LIST_ALL_COMMAND: # API command to get relay statuses - return self.handle_list_all_command() - if command == GET_STATUS_COMMAND: # API command to get relay status - return self.handle_get_status_command(data["pin"]) - if command == UPDATE_COMMAND: # API command to toggle the relay - target = data.get("target") - return self.handle_update_command(data["pin"], target if isinstance(target, bool) else None) - if command == CANCEL_TASK_COMMAND: # API command to cancel the postponed toggling task - return self.handle_cancel_task_command(data["subject"], bool(data["target"]), data["owner"]) - self._logger.warn(f"Unknown command {command}") - return flask.abort(400) # Unknown command + version = int( data.get("version") or data.get("v") or 1 ) + subject_param_name = "pin" if version == 1 else "subject" # todo remove pin when dropping v1 + subject = data.get(subject_param_name) + target = data.get("target") + if command in [GET_STATUS_COMMAND, UPDATE_COMMAND] and subject is None: + return flask.abort(400, description=f"Parameter {subject_param_name} is missing") + # API command to list all the relays with their names and statuses + if command == LIST_ALL_COMMAND: + relays = self.handle_list_all_command() + response = list(map(lambda item: { + "id": item["id"], + "name": item["name"], + "active": item["status"] + }, relays)) if version == 1 else relays # todo remove ternary branch when dropping v1 + self._logger.info(f"Responding {response} to {LIST_ALL_COMMAND} command") + return flask.jsonify(response) + # API command to get relay status + if command == GET_STATUS_COMMAND: + is_closed = False # todo remove this when dropping v1 + try: + is_closed = self.handle_get_status_command(subject) + except HandlingException as exception: + if version != 1: # todo remove condition when dropping v1 + return flask.abort(exception.status) + self._logger.info(f"Responding {is_closed} to {GET_STATUS_COMMAND} command") + return flask.jsonify({ "status": is_closed }) + # API command to toggle the relay + if command == UPDATE_COMMAND: + try: + state = self.handle_update_command(subject, target if isinstance(target, bool) else None) + self._logger.debug(f"Responding {state} to {UPDATE_COMMAND} command") + if version == 1: + return flask.jsonify({ "status": "ok", "result": state }) # todo remove branch when dropping v1 + return flask.jsonify({ "status": state }) + except HandlingException as exception: # todo: deprecate the behavior for 400, only abort in next version + if version == 1 and exception.status == 400: # todo remove this branch when dropping v1 + return flask.jsonify({ "status": "error", "reason": f"Can not toggle the relay {subject}" }) + return flask.abort(exception.status) + # API command to cancel the postponed toggling task + if command == CANCEL_TASK_COMMAND: + cancelled = self.handle_cancel_task_command( + data.get("subject"), bool(target), data["owner"] # todo use subject after dropping v1 + ) + self._logger.debug(f"Responding {cancelled} to {CANCEL_TASK_COMMAND} command") + if version == 1: + return flask.jsonify({ "status": "ok" }) # todo remove this branch when dropping v1 + return flask.jsonify({ "cancelled": cancelled }) + # Unknown commands + self._logger.warn(f"Received unknown API command {command}") + return flask.abort(400, description="Unknown command") def on_event(self, event, payload): self._logger.debug(f"Received the {event} event having payload: {payload}") @@ -221,7 +261,8 @@ def is_printer_relay(self, index) -> bool: def toggle_relay(self, index, target: Optional[bool] = None) -> bool: settings = self._settings.get([index], merged=True) # expensive if not bool(settings["active"]): - raise Exception(f"Relay {index} is disabled") + self._logger.warn(f"Relay {index} is disabled") + raise Exception("Can not toggle a disabled relay") if target is not True and self.is_printer_relay(index): self._logger.debug(f"{index} is the printer relay") if self._printer.is_operational(): @@ -275,15 +316,16 @@ def run_system_command(self, cmd): self._logger.debug(f"Running the system command: {cmd}") os.system(cmd) - def get_upcoming_tasks(self, subjects): + def get_upcoming_tasks(self, subjects: Iterable[str]) -> Dict[str, Optional[Task]]: self._logger.debug("Finding the upcoming tasks") future_tasks = filter( lambda task: task.subject in subjects and task.deadline > time.time() + PREEMPTIVE_CANCELLATION_CUTOFF, self.tasks ) - def reducer(agg, task): + def reducer(agg: Dict[str, Optional[Task]], task: Task): index = task.subject - agg[index] = task if agg[index] is None or task.deadline < agg[index].deadline else agg[index] + current = agg[index] + agg[index] = task if current is None or task.deadline < current.deadline else current return agg return reduce( # { r1: task, r2: None, ... } reducer, @@ -294,7 +336,7 @@ def reducer(agg, task): def update_ui(self): self._logger.debug("Updating the UI") settings = self._settings.get([], merged=True) # expensive - upcoming = self.get_upcoming_tasks(filter( + upcoming_tasks = self.get_upcoming_tasks(filter( lambda index: bool(settings[index]["active"]) and bool(settings[index]["show_upcoming"]), RELAY_INDEXES )) @@ -305,6 +347,7 @@ def update_ui(self): bool(settings[index]["inverted_output"]) ) relay_state = relay.is_closed() if active else False + task = upcoming_tasks[index] self.model[index] = { "relay_pin": relay.pin, "inverted_output": relay.inverted, @@ -313,10 +356,10 @@ def update_ui(self): "active": active, "icon_html": settings[index]["icon_on" if relay_state else "icon_off"], "confirm_off": bool(settings[index]["confirm_off"]) if relay_state else False, - "upcoming": None if upcoming[index] is None else { - "target": upcoming[index].target, - "owner": upcoming[index].owner, - "deadline": int(upcoming[index].deadline * 1000) # ms for JS + "upcoming": None if task is None else { + "target": task.target, + "owner": task.owner, + "deadline": int(task.deadline * 1000) # ms for JS } } self._logger.debug(f"The UI feed: {self.model}") diff --git a/octoprint_octorelay/exceptions.py b/octoprint_octorelay/exceptions.py new file mode 100644 index 00000000..450a64d2 --- /dev/null +++ b/octoprint_octorelay/exceptions.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +class HandlingException(Exception): + def __init__(self, status: int): + super().__init__() + self.status = status diff --git a/octoprint_octorelay/listing.py b/octoprint_octorelay/listing.py new file mode 100644 index 00000000..e9a8ea30 --- /dev/null +++ b/octoprint_octorelay/listing.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from typing import List +# todo after dropping 3.7 take TypedDict from typing instead +from typing_extensions import TypedDict + +# false positive, see https://github.com/pylint-dev/pylint/issues/4166 +# pylint: disable=too-many-ancestors +# pylint: disable=too-few-public-methods +class Element(TypedDict): + id: str + name: str + status: bool + +Listing = List[Element] diff --git a/octoprint_octorelay/model.py b/octoprint_octorelay/model.py new file mode 100644 index 00000000..36204f2d --- /dev/null +++ b/octoprint_octorelay/model.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from typing import Optional, Dict +# todo after dropping 3.7 take TypedDict from typing instead +from typing_extensions import TypedDict + +from .const import RELAY_INDEXES + +# pylint: disable=too-few-public-methods + +class Upcoming(TypedDict): + target: bool + owner: str + deadline: int + +class Entry(TypedDict): + relay_pin: int + inverted_output: bool + relay_state: bool + label_text: str + active: bool + icon_html: str + confirm_off: bool + upcoming: Optional[Upcoming] + +Model = Dict[str, Entry] + +def get_initial_model() -> Model: + return { + index: { + "relay_pin": 0, + "inverted_output": False, + "relay_state": False, + "label_text": index, + "active": False, + "icon_html": index, + "confirm_off": False, + "upcoming": None + } for index in RELAY_INDEXES + } diff --git a/octoprint_octorelay/static/css/.gitkeep b/octoprint_octorelay/static/css/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/octoprint_octorelay/static/css/octorelay.css b/octoprint_octorelay/static/css/octorelay.css deleted file mode 100644 index 5b632cbc..00000000 --- a/octoprint_octorelay/static/css/octorelay.css +++ /dev/null @@ -1,106 +0,0 @@ -#settings_plugin_octorelay .tab-content { - position: relative -} - -#settings_plugin_octorelay .btn > input[type='radio'] { - display: none -} - -#settings_plugin_octorelay input.code { - font-family: monospace; -} - -#settings_plugin_octorelay .input-prepend > .add-on.tiny { - font-size: 0.45rem; - font-weight: 600; -} - -#settings_plugin_octorelay .help-inline a.same-color { - color: inherit; -} - -#settings_plugin_octorelay .control-label span.label, -#settings_plugin_octorelay .help-inline span.label { - zoom: 0.85; /* not scale */ -} - -#settings_plugin_octorelay .fa-info { - border: 2px solid currentColor; - padding: 3px 8px; - border-radius: 50%; - scale: 0.6; - transform-origin: left; -} - -#settings_plugin_octorelay .preview { - display: flex; - width: 24px; - height: 24px; - overflow: hidden; - line-height: unset; - font-size: 1.25rem; - align-items: center; - justify-content: center; - position: absolute; - top: 0; - scale: 3; - transform-origin: top left; - border: 0.3px dashed #00000066; - box-sizing: content-box; - border-radius: 2px; -} - -#settings_plugin_octorelay .preview-caption { - position: absolute; - top: 75px; - width: 75px; - display: flex; - justify-content: center; - align-items: baseline; - gap: 0.5ch; - white-space: nowrap; - scale: 0.75; - transform-origin: top; -} - -#navbar_plugin_octorelay > a { - display: flex; - float: left; - width: 40px; - height: 40px; - padding: unset; - cursor: pointer; - font-size: 1.25rem; - text-decoration: none; - align-items: center; - justify-content: center; -} - -#navbar_plugin_octorelay .popover { - width: auto; -} - -#navbar_plugin_octorelay .popover .popover-title { - display: flex; - align-items: center; - justify-content: space-between; - gap: 24px; -} - -#navbar_plugin_octorelay .popover .popover-content { - display: flex; - flex-direction: column; - gap: 10px; -} - -#navbar_plugin_octorelay .popover .popover-content > div { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; -} - -#navbar_plugin_octorelay .popover-content .btn { - outline: none; - margin-top: unset; -} diff --git a/setup.py b/setup.py index f6a3dea8..d78ea1e5 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,8 @@ def get_version_and_cmdclass(pkg_path): plugin_license = "AGPLv3" # Any additional requirements besides OctoPrint should be listed here -plugin_requires = ["RPi.GPIO"] +# todo after dropping 3.7 remove typing-extensions (used by model.py and listing.py) +plugin_requires = ["RPi.GPIO", "typing-extensions"] # -------------------------------------------------------------------------------------------------------------------- # More advanced options that you usually shouldn't have to touch follow after this point diff --git a/tests/test_init.py b/tests/test_init.py index 11022efc..999d43fd 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -36,7 +36,8 @@ # pylint: disable=wrong-import-position from octoprint_octorelay import ( - OctoRelayPlugin, __plugin_pythoncompat__, __plugin_implementation__, __plugin_hooks__, RELAY_INDEXES, Task + OctoRelayPlugin, __plugin_pythoncompat__, __plugin_implementation__, + __plugin_hooks__, RELAY_INDEXES, Task, HandlingException ) class TestOctoRelayPlugin(unittest.TestCase): @@ -55,8 +56,16 @@ def test_constructor(self): self.assertIsNone(self.plugin_instance.polling_timer) self.assertEqual(self.plugin_instance.tasks, []) self.assertEqual(self.plugin_instance.model, { - "r1": {}, "r2": {}, "r3": {}, "r4": {}, - "r5": {}, "r6": {}, "r7": {}, "r8": {} + index: { + "relay_pin": 0, + "inverted_output": False, + "relay_state": False, + "label_text": index, + "active": False, + "icon_html": index, + "confirm_off": False, + "upcoming": None + } for index in RELAY_INDEXES }) def test_get_settings_version(self): @@ -387,8 +396,8 @@ def test_get_assets(self): def test_get_api_commands(self): # Should return the list of available plugin commands expected = { - "update": [ "pin" ], - "getStatus": [ "pin" ], + "update": [], + "getStatus": [], "listAllStatus": [], "cancelTask": [ "subject", "target", "owner" ] } @@ -562,7 +571,7 @@ def test_toggle_relay__disabled(self): "cmd_on": "CommandON", "cmd_off": "CommandOFF" }) - with self.assertRaises(Exception, msg="Relay r4 is disabled"): + with self.assertRaises(Exception): self.plugin_instance.toggle_relay("r4", True) relayMock.toggle.assert_not_called() @@ -811,22 +820,21 @@ def test_has_switch_permission(self): self.assertIs(actual, case["expected"]) self.plugin_instance._logger.warn.assert_called_with("Failed to check relay switching permission, Caught!") - @patch("flask.jsonify") - def test_handle_list_all_command(self, jsonify_mock): - # Should respond with JSON having states of the active relays + def test_handle_list_all_command(self): + # Should return the active relay states cases = [{ "closed": False, "expectedJson": list(map(lambda index: { "id": index, "name": "TEST", - "active": False + "status": False }, RELAY_INDEXES)) }, { "closed": True, "expectedJson": list(map(lambda index: { "id": index, "name": "TEST", - "active": True + "status": True }, RELAY_INDEXES)) }] for case in cases: @@ -842,12 +850,13 @@ def test_handle_list_all_command(self, jsonify_mock): self.plugin_instance._settings.get = Mock(return_value={ index: relay_settings_mock for index in RELAY_INDEXES }) - self.plugin_instance.handle_list_all_command() - jsonify_mock.assert_called_with(case["expectedJson"]) + self.assertEqual( + self.plugin_instance.handle_list_all_command(), + case["expectedJson"] + ) - @patch("flask.jsonify") - def test_handle_get_status_command(self, jsonify_mock): - # Should respond with JSON having the requested relay state + def test_handle_get_status_command(self): + # Should return the relay state cases = [ { "closed": False, "expectedStatus": False }, { "closed": True, "expectedStatus": True } @@ -863,14 +872,29 @@ def test_handle_get_status_command(self, jsonify_mock): "cmd_off": "CommandOffMock" } self.plugin_instance._settings.get = Mock(return_value=relay_settings_mock) - self.plugin_instance.handle_get_status_command("r4") + self.assertEqual( + self.plugin_instance.handle_get_status_command("r4"), + case["expectedStatus"] + ) self.plugin_instance._settings.get.assert_called_with(["r4"], merged=True) - jsonify_mock.assert_called_with(status=case["expectedStatus"]) - @patch("flask.jsonify") + def test_handle_get_status_command__exception(self): + # Should raise when requesting the state of disabled relay + relay_settings_mock = { + "active": False, + "relay_pin": 17, + "inverted_output": False, + "label_text": "TEST", + "cmd_on": "CommandOnMock", + "cmd_off": "CommandOffMock" + } + self.plugin_instance._settings.get = Mock(return_value=relay_settings_mock) + with self.assertRaises(HandlingException): + self.plugin_instance.handle_get_status_command("r4") + @patch("os.system") - def test_handle_update_command(self, system_mock, jsonify_mock): - # Should toggle the relay state, execute command and update UI when having permission + def test_handle_update_command(self, system_mock): + # Should toggle the relay state, execute command, update UI and return the resulting state self.plugin_instance.update_ui = Mock() self.plugin_instance.is_printer_relay = Mock(return_value=False) cases = [ @@ -878,7 +902,7 @@ def test_handle_update_command(self, system_mock, jsonify_mock): "index": "r4", "target": None, "closed": False, - "expectedStatus": "ok", + "expectedError": False, "expectedResult": True, "expectedToggle": True, "expectedCommand": "CommandOnMock", @@ -888,7 +912,7 @@ def test_handle_update_command(self, system_mock, jsonify_mock): "index": "r4", "target": False, "closed": True, - "expectedStatus": "ok", + "expectedError": False, "expectedResult": False, # from the !closed returned by mocked Relay::toggle() below "expectedToggle": True, "expectedCommand": "CommandOffMock" @@ -897,7 +921,7 @@ def test_handle_update_command(self, system_mock, jsonify_mock): "index": "invalid", "target": False, "closed": True, - "expectedStatus": "error", + "expectedError": True, } ] for case in cases: @@ -915,8 +939,14 @@ def test_handle_update_command(self, system_mock, jsonify_mock): "cmd_off": "CommandOffMock" } self.plugin_instance._settings.get = Mock(return_value=relay_settings_mock) - self.plugin_instance.handle_update_command(case["index"], case["target"]) - if case["expectedStatus"] != "error": + if case["expectedError"]: + with self.assertRaises(HandlingException): + self.plugin_instance.handle_update_command(case["index"], case["target"]) + else: + self.assertEqual( + self.plugin_instance.handle_update_command(case["index"], case["target"]), + case["expectedResult"] + ) self.plugin_instance._settings.get.assert_called_with(["r4"], merged=True) if "expectedToggle" in case: relayMock.toggle.assert_called_with(case["target"]) @@ -927,16 +957,9 @@ def test_handle_update_command(self, system_mock, jsonify_mock): self.plugin_instance.handle_plugin_event.assert_called_with(case["expectedEvent"], scope = ["r4"]) else: self.plugin_instance.handle_plugin_event.assert_not_called() - if "expectedStatus" in case: - if "expectedResult" in case: - jsonify_mock.assert_called_with(status=case["expectedStatus"], result=case["expectedResult"]) - else: - jsonify_mock.assert_called_with(status=case["expectedStatus"]) - - @patch("flask.abort") - def test_handle_update_command__exception_permissions(self, abort_mock): - # Should refuse to update the relay state in case of insufficient permissions + def test_handle_update_command__exception_permissions(self): + # Should raise in case of insufficient permissions self.plugin_instance._settings.get = Mock(return_value={ "active": True, "relay_pin": 17, @@ -945,13 +968,12 @@ def test_handle_update_command__exception_permissions(self, abort_mock): "cmd_off": "CommandOffMock" }) permissionsMock.PLUGIN_OCTORELAY_SWITCH.can = Mock(return_value=False) - self.plugin_instance.handle_update_command("r4") + with self.assertRaises(HandlingException): + self.plugin_instance.handle_update_command("r4") permissionsMock.PLUGIN_OCTORELAY_SWITCH.can.assert_called_with() - abort_mock.assert_called_with(403) - @patch("flask.jsonify") - def test_handle_update_command__exception_disabled(self, jsonify_mock): - # Should refuse to update the disabled relay + def test_handle_update_command__exception_disabled(self): + # Should raise when attempting to update a disabled relay self.plugin_instance._settings.get = Mock(return_value={ "active": False, "relay_pin": 17, @@ -960,59 +982,177 @@ def test_handle_update_command__exception_disabled(self, jsonify_mock): "cmd_off": "CommandOffMock" }) permissionsMock.PLUGIN_OCTORELAY_SWITCH.can = Mock(return_value=True) - self.plugin_instance.handle_update_command("r4") + with self.assertRaises(HandlingException): + self.plugin_instance.handle_update_command("r4") permissionsMock.PLUGIN_OCTORELAY_SWITCH.can.assert_called_with() - jsonify_mock.assert_called_with(status="error", reason="Can not toggle the relay r4") - @patch("flask.jsonify") - def test_handle_cancel_task_command(self, jsonify_mock): + def test_handle_cancel_task_command(self): + # Should return boolean indicating that the task was cancelled self.plugin_instance.update_ui = Mock() self.plugin_instance.cancel_tasks = Mock() - self.plugin_instance.handle_cancel_task_command("r4", True, "STARTUP") + self.assertTrue( + self.plugin_instance.handle_cancel_task_command("r4", True, "STARTUP") + ) self.plugin_instance.cancel_tasks.assert_called_with("r4", "USER_ACTION", True, "STARTUP") self.plugin_instance.update_ui.assert_called_with() - jsonify_mock.assert_called_with(status="ok") - def test_on_api_command(self): - self.plugin_instance.handle_list_all_command = Mock() - self.plugin_instance.handle_get_status_command = Mock() - self.plugin_instance.handle_update_command = Mock() - self.plugin_instance.handle_cancel_task_command = Mock() + @patch("flask.jsonify") + def test_on_api_command(self, jsonify_mock): + # Should call a handler and respond with expected payload + self.plugin_instance.handle_list_all_command = Mock(return_value=[ + {"id": "r1", "name": "Test", "status": True} + ]) + self.plugin_instance.handle_get_status_command = Mock(return_value=True) + self.plugin_instance.handle_update_command = Mock(return_value=False) + self.plugin_instance.handle_cancel_task_command = Mock(return_value=True) cases = [ { "command": "listAllStatus", "data": {}, - "expectedCall": self.plugin_instance.handle_list_all_command, - "expectedParams": [] + "expectedMethod": self.plugin_instance.handle_list_all_command, + "expectedArguments": [], + "expectedOutcome": jsonify_mock, + "expectedPayload": [{"id": "r1", "name": "Test", "active": True}], + }, + { + "command": "listAllStatus", + "data": {"v": 2}, + "expectedMethod": self.plugin_instance.handle_list_all_command, + "expectedArguments": [], + "expectedOutcome": jsonify_mock, + "expectedPayload": [{"id": "r1", "name": "Test", "status": True}], }, { "command": "getStatus", "data": { "pin": "r4" }, - "expectedCall": self.plugin_instance.handle_get_status_command, - "expectedParams": ["r4"] + "expectedMethod": self.plugin_instance.handle_get_status_command, + "expectedArguments": ["r4"], + "expectedOutcome": jsonify_mock, + "expectedPayload": {"status": True}, + }, + { + "command": "getStatus", + "data": { "version": 2, "subject": "r4" }, + "expectedMethod": self.plugin_instance.handle_get_status_command, + "expectedArguments": ["r4"], + "expectedOutcome": jsonify_mock, + "expectedPayload": {"status": True}, }, { "command": "update", "data": { "pin": "r4", "target": True }, - "expectedCall": self.plugin_instance.handle_update_command, - "expectedParams": ["r4", True] + "expectedMethod": self.plugin_instance.handle_update_command, + "expectedArguments": ["r4", True], + "expectedOutcome": jsonify_mock, + "expectedPayload": {"status": "ok", "result": False}, + }, + { + "command": "update", + "data": { "v": 2, "subject": "r4", "target": True }, + "expectedMethod": self.plugin_instance.handle_update_command, + "expectedArguments": ["r4", True], + "expectedOutcome": jsonify_mock, + "expectedPayload": {"status": False}, }, { "command": "cancelTask", "data": { "subject": "r4", "owner": "STARTUP", "target": True }, - "expectedCall": self.plugin_instance.handle_cancel_task_command, - "expectedParams": [ "r4", True, "STARTUP" ] + "expectedMethod": self.plugin_instance.handle_cancel_task_command, + "expectedArguments": [ "r4", True, "STARTUP" ], + "expectedOutcome": jsonify_mock, + "expectedPayload": {"status": "ok"}, + }, + { + "command": "cancelTask", + "data": { "v": 2, "subject": "r4", "owner": "STARTUP", "target": True }, + "expectedMethod": self.plugin_instance.handle_cancel_task_command, + "expectedArguments": [ "r4", True, "STARTUP" ], + "expectedOutcome": jsonify_mock, + "expectedPayload": {"cancelled": True}, } ] for case in cases: + case["expectedMethod"].reset_mock() + case["expectedOutcome"].reset_mock() self.plugin_instance.on_api_command(case["command"], case["data"]) - case["expectedCall"].assert_called_with(*case["expectedParams"]) + case["expectedMethod"].assert_called_with(*case["expectedArguments"]) + case["expectedOutcome"].assert_called_with(case["expectedPayload"]) + + @patch("flask.abort") + @patch("flask.jsonify") + def test_on_api_command__update_exceptions(self, jsonify_mock, abort_mock): + # Should respond with a faulty HTTP code or status error when handler raises + cases = [ + { + "payload": { "pin": "r4" }, + "status": 403, + "expectedMethod": abort_mock, + "expectedArgument": 403 + }, + { + "payload": { "version": 2, "subject": "r4" }, + "status": 403, + "expectedMethod": abort_mock, + "expectedArgument": 403 + }, + { + "payload": { "pin": "r4" }, + "status": 400, + "expectedMethod": jsonify_mock, + "expectedArgument": { "status": "error", "reason": "Can not toggle the relay r4" } + }, + { + "payload": { "v": 2, "subject": "r4" }, + "status": 400, + "expectedMethod": abort_mock, + "expectedArgument": 400 + } + ] + for case in cases: + case["expectedMethod"].reset_mock() + self.plugin_instance.handle_update_command = Mock(side_effect=HandlingException(case["status"])) + self.plugin_instance.on_api_command("update", case["payload"]) + case["expectedMethod"].assert_called_with(case["expectedArgument"]) + + @patch("flask.abort") + @patch("flask.jsonify") + def test_om_api_command__get_status_exception(self, jsonify_mock, abort_mock): + # Should respond with status false when handler raises + cases = [ + { + "payload": { "pin": "r4" }, + "expectedMethod": jsonify_mock, + "expectedArgument": {"status": False} + }, + { + "payload": { "v": 2, "subject": "r4" }, + "expectedMethod": abort_mock, + "expectedArgument": 400 + } + ] + self.plugin_instance.handle_get_status_command = Mock(side_effect=HandlingException(400)) + for case in cases: + case["expectedMethod"].reset_mock() + self.plugin_instance.on_api_command("getStatus", case["payload"]) + case["expectedMethod"].assert_called_with(case["expectedArgument"]) + + @patch("flask.abort") + def test_on_api_command__missing_parameters(self, abort_mock): + cases = [ + { "command": "getStatus", "version": 1, "missing": "pin" }, + { "command": "update", "version": 1, "missing": "pin" }, + { "command": "getStatus", "version": 2, "missing": "subject" }, + { "command": "update", "version": 2, "missing": "subject" } + ] + for case in cases: + self.plugin_instance.on_api_command(case["command"], { "version": case["version"] }) + abort_mock.assert_called_with(400, description=f"Parameter {case['missing']} is missing") @patch("flask.abort") def test_on_api_command__unknown(self, abort_mock): # Should respond with status code 400 (bad request) to unknown commands self.plugin_instance.on_api_command("command", {}) - abort_mock.assert_called_with(400) + abort_mock.assert_called_with(400, description="Unknown command") def test_process_at_command(self): # Should toggle the relay having index supplied as a parameter diff --git a/ts/helpers/actions.spec.ts b/ts/helpers/actions.spec.ts index 965cf1ae..836ab199 100644 --- a/ts/helpers/actions.spec.ts +++ b/ts/helpers/actions.spec.ts @@ -30,7 +30,8 @@ describe("Actions", () => { upcoming: null, }); expect(apiMock).toHaveBeenCalledWith("octorelay", "update", { - pin: "r1", + v: 2, + subject: "r1", }); }); @@ -56,7 +57,8 @@ describe("Actions", () => { elementMock.modal.mockClear(); elementMock.on.mock.calls[1][1](); // confirm expect(apiMock).toHaveBeenCalledWith("octorelay", "update", { - pin: "r2", + v: 2, + subject: "r2", }); expect(elementMock.modal).toHaveBeenCalledWith("hide"); }); @@ -70,6 +72,7 @@ describe("Actions", () => { deadline: 86400, }); expect(apiMock).toHaveBeenCalledWith("octorelay", "cancelTask", { + v: 2, owner: "PRINTING_STARTED", subject: "r2", target: true, diff --git a/ts/helpers/actions.ts b/ts/helpers/actions.ts index 949a5c9e..7a85d000 100644 --- a/ts/helpers/actions.ts +++ b/ts/helpers/actions.ts @@ -3,7 +3,7 @@ import { ownCode } from "./const"; export const toggleRelay = (key: string, relay: Relay) => { const command = () => - OctoPrint.simpleApiCommand(ownCode, "update", { pin: key }); + OctoPrint.simpleApiCommand(ownCode, "update", { v: 2, subject: key }); if (!relay.confirm_off) { return command(); } @@ -28,6 +28,7 @@ export const toggleRelay = (key: string, relay: Relay) => { export const cancelTask = (key: string, { owner, target }: Task) => OctoPrint.simpleApiCommand(ownCode, "cancelTask", { + v: 2, subject: key, owner, target, diff --git a/ts/package.json b/ts/package.json index b8339198..02e9cbac 100644 --- a/ts/package.json +++ b/ts/package.json @@ -11,7 +11,9 @@ "scripts": { "lint": "eslint ./", "test": "tsc --noEmit && TZ=UTC jest", - "build": "tsup" + "build": "yarn build:js && yarn build:styles", + "build:js": "tsup", + "build:styles": "sass styles/octorelay.scss ../octoprint_octorelay/static/css/octorelay.css --no-source-map" }, "dependencies": {}, "devDependencies": { @@ -33,6 +35,7 @@ "jsdom": "^22.1.0", "mockdate": "^3.0.5", "prettier": "^3.0.3", + "sass": "^1.68.0", "tsup": "^7.2.0", "typescript": "^5.2.2" } diff --git a/ts/styles/navbar.scss b/ts/styles/navbar.scss new file mode 100644 index 00000000..d622c48f --- /dev/null +++ b/ts/styles/navbar.scss @@ -0,0 +1,43 @@ +#navbar_plugin_octorelay { + & > a { + display: flex; + float: left; + width: 40px; + height: 40px; + padding: unset; + cursor: pointer; + font-size: 1.25rem; + text-decoration: none; + align-items: center; + justify-content: center; + } + + & .popover { + width: auto; + + & .popover-title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + } + + & .popover-content { + display: flex; + flex-direction: column; + gap: 10px; + + & > div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + } + + & .btn { + outline: none; + margin-top: unset; + } + } + } +} diff --git a/ts/styles/octorelay.scss b/ts/styles/octorelay.scss new file mode 100644 index 00000000..dd922b5e --- /dev/null +++ b/ts/styles/octorelay.scss @@ -0,0 +1,2 @@ +@import "settings"; +@import "navbar"; diff --git a/ts/styles/settings.scss b/ts/styles/settings.scss new file mode 100644 index 00000000..ecb06b3c --- /dev/null +++ b/ts/styles/settings.scss @@ -0,0 +1,66 @@ +#settings_plugin_octorelay { + & .tab-content { + position: relative + } + + & .btn > input[type='radio'] { + display: none + } + + & input.code { + font-family: monospace; + } + + & .input-prepend > .add-on.tiny { + font-size: 0.45rem; + font-weight: 600; + } + + & .help-inline a.same-color { + color: inherit; + } + + & .control-label span.label, + & .help-inline span.label { + zoom: 0.85; // not scale + } + + & .fa-info { + border: 2px solid currentColor; + padding: 3px 8px; + border-radius: 50%; + scale: 0.6; + transform-origin: left; + } + + & .preview { + display: flex; + width: 24px; + height: 24px; + overflow: hidden; + line-height: unset; + font-size: 1.25rem; + align-items: center; + justify-content: center; + position: absolute; + top: 0; + scale: 3; + transform-origin: top left; + border: 0.3px dashed #00000066; + box-sizing: content-box; + border-radius: 2px; + } + + & .preview-caption { + position: absolute; + top: 75px; + width: 75px; + display: flex; + justify-content: center; + align-items: baseline; + gap: 0.5ch; + white-space: nowrap; + scale: 0.75; + transform-origin: top; + } +} diff --git a/ts/yarn.lock b/ts/yarn.lock index 5925545f..516b297e 100644 --- a/ts/yarn.lock +++ b/ts/yarn.lock @@ -24,24 +24,24 @@ chalk "^2.4.2" "@babel/compat-data@^7.22.9": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.20.tgz#8df6e96661209623f1975d66c35ffca66f3306d0" - integrity sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw== + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.2.tgz#6a12ced93455827037bfb5ed8492820d60fc32cc" + integrity sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ== "@babel/core@^7.11.6", "@babel/core@^7.12.3": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.0.tgz#f8259ae0e52a123eb40f552551e647b506a94d83" - integrity sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ== + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.2.tgz#ed10df0d580fff67c5f3ee70fd22e2e4c90a9f94" + integrity sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ== dependencies: "@ampproject/remapping" "^2.2.0" "@babel/code-frame" "^7.22.13" "@babel/generator" "^7.23.0" "@babel/helper-compilation-targets" "^7.22.15" "@babel/helper-module-transforms" "^7.23.0" - "@babel/helpers" "^7.23.0" + "@babel/helpers" "^7.23.2" "@babel/parser" "^7.23.0" "@babel/template" "^7.22.15" - "@babel/traverse" "^7.23.0" + "@babel/traverse" "^7.23.2" "@babel/types" "^7.23.0" convert-source-map "^2.0.0" debug "^4.1.0" @@ -142,13 +142,13 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040" integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA== -"@babel/helpers@^7.23.0": - version "7.23.1" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.1.tgz#44e981e8ce2b9e99f8f0b703f3326a4636c16d15" - integrity sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA== +"@babel/helpers@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.2.tgz#2832549a6e37d484286e15ba36a5330483cac767" + integrity sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ== dependencies: "@babel/template" "^7.22.15" - "@babel/traverse" "^7.23.0" + "@babel/traverse" "^7.23.2" "@babel/types" "^7.23.0" "@babel/highlight@^7.22.13": @@ -272,10 +272,10 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" -"@babel/traverse@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.0.tgz#18196ddfbcf4ccea324b7f6d3ada00d8c5a99c53" - integrity sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw== +"@babel/traverse@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== dependencies: "@babel/code-frame" "^7.22.13" "@babel/generator" "^7.23.0" @@ -773,74 +773,74 @@ dependencies: "@sinonjs/commons" "^3.0.0" -"@swc/core-darwin-arm64@1.3.92": - version "1.3.92" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.92.tgz#0498d3584cf877e39107c94705c38fa4a8c04789" - integrity sha512-v7PqZUBtIF6Q5Cp48gqUiG8zQQnEICpnfNdoiY3xjQAglCGIQCjJIDjreZBoeZQZspB27lQN4eZ43CX18+2SnA== - -"@swc/core-darwin-x64@1.3.92": - version "1.3.92" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.3.92.tgz#1728e7ebbfe37b56c07d99e29dde78bfa90cf8d1" - integrity sha512-Q3XIgQfXyxxxms3bPN+xGgvwk0TtG9l89IomApu+yTKzaIIlf051mS+lGngjnh9L0aUiCp6ICyjDLtutWP54fw== - -"@swc/core-linux-arm-gnueabihf@1.3.92": - version "1.3.92" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.92.tgz#6f7c20833b739f8911c936c9783976ded2c449dc" - integrity sha512-tnOCoCpNVXC+0FCfG84PBZJyLlz0Vfj9MQhyhCvlJz9hQmvpf8nTdKH7RHrOn8VfxtUBLdVi80dXgIFgbvl7qA== - -"@swc/core-linux-arm64-gnu@1.3.92": - version "1.3.92" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.92.tgz#bb01dd9b922b0c076c38924013bd10036ce39c7c" - integrity sha512-lFfGhX32w8h1j74Iyz0Wv7JByXIwX11OE9UxG+oT7lG0RyXkF4zKyxP8EoxfLrDXse4Oop434p95e3UNC3IfCw== - -"@swc/core-linux-arm64-musl@1.3.92": - version "1.3.92" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.92.tgz#0070165eed2805475c98eb732bab8bdca955932e" - integrity sha512-rOZtRcLj57MSAbiecMsqjzBcZDuaCZ8F6l6JDwGkQ7u1NYR57cqF0QDyU7RKS1Jq27Z/Vg21z5cwqoH5fLN+Sg== - -"@swc/core-linux-x64-gnu@1.3.92": - version "1.3.92" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.92.tgz#d9785f93b9121eeef0f54e8d845dd216698e0115" - integrity sha512-qptoMGnBL6v89x/Qpn+l1TH1Y0ed+v0qhNfAEVzZvCvzEMTFXphhlhYbDdpxbzRmCjH6GOGq7Y+xrWt9T1/ARg== - -"@swc/core-linux-x64-musl@1.3.92": - version "1.3.92" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.92.tgz#8fe5cf244695bf4f0bc7dc7df450a9bd1bfccc2b" - integrity sha512-g2KrJ43bZkCZHH4zsIV5ErojuV1OIpUHaEyW1gf7JWKaFBpWYVyubzFPvPkjcxHGLbMsEzO7w/NVfxtGMlFH/Q== - -"@swc/core-win32-arm64-msvc@1.3.92": - version "1.3.92" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.92.tgz#d6150785455c813a8e62f4e4b0a22773baf398eb" - integrity sha512-3MCRGPAYDoQ8Yyd3WsCMc8eFSyKXY5kQLyg/R5zEqA0uthomo0m0F5/fxAJMZGaSdYkU1DgF73ctOWOf+Z/EzQ== - -"@swc/core-win32-ia32-msvc@1.3.92": - version "1.3.92" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.92.tgz#8142166bceafbaa209d440b36fdc8cd4b4f82768" - integrity sha512-zqTBKQhgfWm73SVGS8FKhFYDovyRl1f5dTX1IwSKynO0qHkRCqJwauFJv/yevkpJWsI2pFh03xsRs9HncTQKSA== - -"@swc/core-win32-x64-msvc@1.3.92": - version "1.3.92" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.92.tgz#4ba542875fc690b579232721ccec7873e139646a" - integrity sha512-41bE66ddr9o/Fi1FBh0sHdaKdENPTuDpv1IFHxSg0dJyM/jX8LbkjnpdInYXHBxhcLVAPraVRrNsC4SaoPw2Pg== +"@swc/core-darwin-arm64@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.93.tgz#aefd94625451988286bebccb1c072bae0a36bcdb" + integrity sha512-gEKgk7FVIgltnIfDO6GntyuQBBlAYg5imHpRgLxB1zSI27ijVVkksc6QwISzFZAhKYaBWIsFSVeL9AYSziAF7A== + +"@swc/core-darwin-x64@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.3.93.tgz#18409c6effdf508ddf1ebccfa77d35aaa6cd72f0" + integrity sha512-ZQPxm/fXdDQtn3yrYSL/gFfA8OfZ5jTi33yFQq6vcg/Y8talpZ+MgdSlYM0FkLrZdMTYYTNFiuBQuuvkA+av+Q== + +"@swc/core-linux-arm-gnueabihf@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.93.tgz#23a97bc94a8b2f23fb6cc4bc9d8936899e5eeff5" + integrity sha512-OYFMMI2yV+aNe3wMgYhODxHdqUB/jrK0SEMHHS44GZpk8MuBXEF+Mcz4qjkY5Q1EH7KVQqXb/gVWwdgTHpjM2A== + +"@swc/core-linux-arm64-gnu@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.93.tgz#7a17406a7cf76a959a617626d5ee2634ae9afa26" + integrity sha512-BT4dT78odKnJMNiq5HdjBsv29CiIdcCcImAPxeFqAeFw1LL6gh9nzI8E96oWc+0lVT5lfhoesCk4Qm7J6bty8w== + +"@swc/core-linux-arm64-musl@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.93.tgz#a30be7780090afefd3b8706398418cbe1d23db49" + integrity sha512-yH5fWEl1bktouC0mhh0Chuxp7HEO4uCtS/ly1Vmf18gs6wZ8DOOkgAEVv2dNKIryy+Na++ljx4Ym7C8tSJTrLw== + +"@swc/core-linux-x64-gnu@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.93.tgz#41e903fd82e059952d16051b442cbe65ee5b8cb3" + integrity sha512-OFUdx64qvrGJhXKEyxosHxgoUVgba2ztYh7BnMiU5hP8lbI8G13W40J0SN3CmFQwPP30+3oEbW7LWzhKEaYjlg== + +"@swc/core-linux-x64-musl@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.93.tgz#0866807545c44eac9b3254b374310ad5e1c573f9" + integrity sha512-4B8lSRwEq1XYm6xhxHhvHmKAS7pUp1Q7E33NQ2TlmFhfKvCOh86qvThcjAOo57x8DRwmpvEVrqvpXtYagMN6Ig== + +"@swc/core-win32-arm64-msvc@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.93.tgz#c72411dea2fd4f62a832f71a6e15424d849e7610" + integrity sha512-BHShlxtkven8ZjjvZ5QR6sC5fZCJ9bMujEkiha6W4cBUTY7ce7qGFyHmQd+iPC85d9kD/0cCiX/Xez8u0BhO7w== + +"@swc/core-win32-ia32-msvc@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.93.tgz#05c2b031b976af4ef81f5073ee114254678a5d5d" + integrity sha512-nEwNWnz4JzYAK6asVvb92yeylfxMYih7eMQOnT7ZVlZN5ba9WF29xJ6kcQKs9HRH6MvWhz9+wRgv3FcjlU6HYA== + +"@swc/core-win32-x64-msvc@1.3.93": + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.93.tgz#f8748b3fd1879f13084b1b0814edf328c662935c" + integrity sha512-jibQ0zUr4kwJaQVwgmH+svS04bYTPnPw/ZkNInzxS+wFAtzINBYcU8s2PMWbDb2NGYiRSEeoSGyAvS9H+24JFA== "@swc/core@^1.3.92": - version "1.3.92" - resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.3.92.tgz#f51808cdb6cbb90b0877b9a51806eea9a70eafca" - integrity sha512-vx0vUrf4YTEw59njOJ46Ha5i0cZTMYdRHQ7KXU29efN1MxcmJH2RajWLPlvQarOP1ab9iv9cApD7SMchDyx2vA== + version "1.3.93" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.3.93.tgz#be4282aa44deffb0e5081a2613bac00335600630" + integrity sha512-690GRr1wUGmGYZHk7fUduX/JUwViMF2o74mnZYIWEcJaCcd9MQfkhsxPBtjeg6tF+h266/Cf3RPYhsFBzzxXcA== dependencies: "@swc/counter" "^0.1.1" "@swc/types" "^0.1.5" optionalDependencies: - "@swc/core-darwin-arm64" "1.3.92" - "@swc/core-darwin-x64" "1.3.92" - "@swc/core-linux-arm-gnueabihf" "1.3.92" - "@swc/core-linux-arm64-gnu" "1.3.92" - "@swc/core-linux-arm64-musl" "1.3.92" - "@swc/core-linux-x64-gnu" "1.3.92" - "@swc/core-linux-x64-musl" "1.3.92" - "@swc/core-win32-arm64-msvc" "1.3.92" - "@swc/core-win32-ia32-msvc" "1.3.92" - "@swc/core-win32-x64-msvc" "1.3.92" + "@swc/core-darwin-arm64" "1.3.93" + "@swc/core-darwin-x64" "1.3.93" + "@swc/core-linux-arm-gnueabihf" "1.3.93" + "@swc/core-linux-arm64-gnu" "1.3.93" + "@swc/core-linux-arm64-musl" "1.3.93" + "@swc/core-linux-x64-gnu" "1.3.93" + "@swc/core-linux-x64-musl" "1.3.93" + "@swc/core-win32-arm64-msvc" "1.3.93" + "@swc/core-win32-ia32-msvc" "1.3.93" + "@swc/core-win32-x64-msvc" "1.3.93" "@swc/counter@^0.1.1": version "0.1.2" @@ -964,9 +964,11 @@ integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/node@*": - version "20.8.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.3.tgz#c4ae2bb1cfab2999ed441a95c122bbbe1567a66d" - integrity sha512-jxiZQFpb+NlH5kjW49vXxvxTjeeqlbsnTAdBTKpzEdPs9itay7MscYXz3Fo9VYFEsfQ6LJFitHad3faerLAjCw== + version "20.8.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.6.tgz#0dbd4ebcc82ad0128df05d0e6f57e05359ee47fa" + integrity sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ== + dependencies: + undici-types "~5.25.1" "@types/semver@^7.5.0": version "7.5.3" @@ -1008,15 +1010,15 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^6.7.4": - version "6.7.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz#057338df21b6062c2f2fc5999fbea8af9973ac6d" - integrity sha512-DAbgDXwtX+pDkAHwiGhqP3zWUGpW49B7eqmgpPtg+BKJXwdct79ut9+ifqOFPJGClGKSHXn2PTBatCnldJRUoA== + version "6.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz#06abe4265e7c82f20ade2dcc0e3403c32d4f148b" + integrity sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw== dependencies: "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.7.4" - "@typescript-eslint/type-utils" "6.7.4" - "@typescript-eslint/utils" "6.7.4" - "@typescript-eslint/visitor-keys" "6.7.4" + "@typescript-eslint/scope-manager" "6.8.0" + "@typescript-eslint/type-utils" "6.8.0" + "@typescript-eslint/utils" "6.8.0" + "@typescript-eslint/visitor-keys" "6.8.0" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.2.4" @@ -1025,71 +1027,71 @@ ts-api-utils "^1.0.1" "@typescript-eslint/parser@^6.7.4": - version "6.7.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.7.4.tgz#23d1dd4fe5d295c7fa2ab651f5406cd9ad0bd435" - integrity sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA== - dependencies: - "@typescript-eslint/scope-manager" "6.7.4" - "@typescript-eslint/types" "6.7.4" - "@typescript-eslint/typescript-estree" "6.7.4" - "@typescript-eslint/visitor-keys" "6.7.4" + version "6.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.8.0.tgz#bb2a969d583db242f1ee64467542f8b05c2e28cb" + integrity sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg== + dependencies: + "@typescript-eslint/scope-manager" "6.8.0" + "@typescript-eslint/types" "6.8.0" + "@typescript-eslint/typescript-estree" "6.8.0" + "@typescript-eslint/visitor-keys" "6.8.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@6.7.4": - version "6.7.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.7.4.tgz#a484a17aa219e96044db40813429eb7214d7b386" - integrity sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A== +"@typescript-eslint/scope-manager@6.8.0": + version "6.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.8.0.tgz#5cac7977385cde068ab30686889dd59879811efd" + integrity sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g== dependencies: - "@typescript-eslint/types" "6.7.4" - "@typescript-eslint/visitor-keys" "6.7.4" + "@typescript-eslint/types" "6.8.0" + "@typescript-eslint/visitor-keys" "6.8.0" -"@typescript-eslint/type-utils@6.7.4": - version "6.7.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.7.4.tgz#847cd3b59baf948984499be3e0a12ff07373e321" - integrity sha512-n+g3zi1QzpcAdHFP9KQF+rEFxMb2KxtnJGID3teA/nxKHOVi3ylKovaqEzGBbVY2pBttU6z85gp0D00ufLzViQ== +"@typescript-eslint/type-utils@6.8.0": + version "6.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.8.0.tgz#50365e44918ca0fd159844b5d6ea96789731e11f" + integrity sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g== dependencies: - "@typescript-eslint/typescript-estree" "6.7.4" - "@typescript-eslint/utils" "6.7.4" + "@typescript-eslint/typescript-estree" "6.8.0" + "@typescript-eslint/utils" "6.8.0" debug "^4.3.4" ts-api-utils "^1.0.1" -"@typescript-eslint/types@6.7.4": - version "6.7.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.4.tgz#5d358484d2be986980c039de68e9f1eb62ea7897" - integrity sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA== +"@typescript-eslint/types@6.8.0": + version "6.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.8.0.tgz#1ab5d4fe1d613e3f65f6684026ade6b94f7e3ded" + integrity sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ== -"@typescript-eslint/typescript-estree@6.7.4": - version "6.7.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.4.tgz#f2baece09f7bb1df9296e32638b2e1130014ef1a" - integrity sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ== +"@typescript-eslint/typescript-estree@6.8.0": + version "6.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.8.0.tgz#9565f15e0cd12f55cf5aa0dfb130a6cb0d436ba1" + integrity sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg== dependencies: - "@typescript-eslint/types" "6.7.4" - "@typescript-eslint/visitor-keys" "6.7.4" + "@typescript-eslint/types" "6.8.0" + "@typescript-eslint/visitor-keys" "6.8.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@6.7.4": - version "6.7.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.7.4.tgz#2236f72b10e38277ee05ef06142522e1de470ff2" - integrity sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA== +"@typescript-eslint/utils@6.8.0": + version "6.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.8.0.tgz#d42939c2074c6b59844d0982ce26a51d136c4029" + integrity sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.7.4" - "@typescript-eslint/types" "6.7.4" - "@typescript-eslint/typescript-estree" "6.7.4" + "@typescript-eslint/scope-manager" "6.8.0" + "@typescript-eslint/types" "6.8.0" + "@typescript-eslint/typescript-estree" "6.8.0" semver "^7.5.4" -"@typescript-eslint/visitor-keys@6.7.4": - version "6.7.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.4.tgz#80dfecf820fc67574012375859085f91a4dff043" - integrity sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA== +"@typescript-eslint/visitor-keys@6.8.0": + version "6.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.8.0.tgz#cffebed56ae99c45eba901c378a6447b06be58b8" + integrity sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg== dependencies: - "@typescript-eslint/types" "6.7.4" + "@typescript-eslint/types" "6.8.0" eslint-visitor-keys "^3.4.1" abab@^2.0.6: @@ -1420,9 +1422,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001541: - version "1.0.30001546" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz#10fdad03436cfe3cc632d3af7a99a0fb497407f0" - integrity sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw== + version "1.0.30001549" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001549.tgz#7d1a3dce7ea78c06ed72c32c2743ea364b3615aa" + integrity sha512-qRp48dPYSCYaP+KurZLhDYdVE+yEyht/3NlmcJgVQ2VMGt6JL36ndQ/7rgspdZsJuxDPFIo/OzBT2+GmIJ53BA== chalk@^2.4.2: version "2.4.2" @@ -1446,7 +1448,7 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== -chokidar@^3.5.1: +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.1: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -1632,9 +1634,9 @@ default-browser@^4.0.0: titleize "^3.0.0" define-data-property@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.0.tgz#0db13540704e1d8d479a0656cf781267531b9451" - integrity sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g== + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== dependencies: get-intrinsic "^1.2.1" gopd "^1.0.1" @@ -1698,9 +1700,9 @@ domexception@^4.0.0: webidl-conversions "^7.0.0" electron-to-chromium@^1.4.535: - version "1.4.544" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.544.tgz#fcb156d83f0ee6e4c9d030c6fedb2a37594f3abf" - integrity sha512-54z7squS1FyFRSUqq/knOFSptjjogLZXbKcYk3B0qkE1KZzvqASwRZnY2KzZQJqIYLVD38XZeoiMRflYSwyO4w== + version "1.4.557" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.557.tgz#f3941b569c82b7bb909411855c6ff9bfe1507829" + integrity sha512-6x0zsxyMXpnMJnHrondrD3SuAeKcwij9S+83j2qHAQPXbGTDDfgImzzwgGlzrIcXbHQ42tkG4qA6U860cImNhw== emittery@^0.13.1: version "0.13.1" @@ -1904,9 +1906,9 @@ eslint-plugin-import@^2.28.1: tsconfig-paths "^3.14.2" eslint-plugin-prettier@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz#6887780ed95f7708340ec79acfdf60c35b9be57a" - integrity sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w== + version "5.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz#a3b399f04378f79f066379f544e42d6b73f11515" + integrity sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg== dependencies: prettier-linter-helpers "^1.0.0" synckit "^0.8.5" @@ -2167,9 +2169,9 @@ fsevents@^2.3.2, fsevents@~2.3.2: integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== function.prototype.name@^1.1.6: version "1.1.6" @@ -2405,6 +2407,11 @@ ignore@^5.2.0, ignore@^5.2.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +immutable@^4.0.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f" + integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA== + import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -3359,9 +3366,9 @@ object-assign@^4.0.1: integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== object-inspect@^1.12.3, object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + version "1.13.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.0.tgz#42695d3879e1cd5bda6df5062164d80c996e23e2" + integrity sha512-HQ4J+ic8hKrgIt3mqk6cVOVrW2ozL4KdvHlqpBv9vDYWx9ysAgENAdvy4FoGF+KFdhR7nQTNm5J0ctAeOwn+3g== object-keys@^1.1.1: version "1.1.1" @@ -3688,9 +3695,9 @@ resolve.exports@^2.0.0: integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== resolve@^1.20.0, resolve@^1.22.4: - version "1.22.6" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362" - integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw== + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: is-core-module "^2.13.0" path-parse "^1.0.7" @@ -3758,6 +3765,15 @@ safe-regex-test@^1.0.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sass@^1.68.0: + version "1.69.3" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.3.tgz#f8a0c488697e6419519834a13335e7b65a609c11" + integrity sha512-X99+a2iGdXkdWn1akFPs0ZmelUzyAQfvqYc2P/MPTrJRuIRoTffGzT9W9nFqG00S+c8hXzVmgxhUuHFdrwxkhQ== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + saxes@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" @@ -3822,6 +3838,11 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +"source-map-js@>=0.6.2 <2.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" @@ -4182,6 +4203,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~5.25.1: + version "5.25.3" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3" + integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA== + universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" @@ -4359,9 +4385,9 @@ yallist@^4.0.0: integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@^2.1.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.2.tgz#f522db4313c671a0ca963a75670f1c12ea909144" - integrity sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg== + version "2.3.3" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.3.tgz#01f6d18ef036446340007db8e016810e5d64aad9" + integrity sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ== yargs-parser@^21.1.1: version "21.1.1"